Compare commits

...
489 Commits
Author SHA1 Message Date
José Valim 3bb5f92fa9 Release v1.20.0-rc.1 2026-01-13 11:46:20 +01:00
dependabot[bot] cb472c10f3 Bump actions/checkout from 6.0.0 to 6.0.1 (#15059)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

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

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

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

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

        # 3
        0

    Attempted function clauses (showing 1 out of 1):

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 12:49:13 +01:00
dependabot[bot] 7a01f5bab2 Bump actions/download-artifact from 6.0.0 to 7.0.0 (#15016)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/018cc2cf5baa6db3ef3c5f8a56943fffe632ef53...37930b1c2abaa49bbe596cd826c3c89aef350131)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 12:48:48 +01:00
dependabot[bot] f7904a4aa6 Bump actions/upload-artifact from 5.0.0 to 6.0.0 (#15015)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/330a01c490aca151604b8cf639adc76d48f6c5d4...b7c566a772e6b6bfb58ed0dc250532a479d7789f)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 12:48:39 +01:00
dependabot[bot] 6948d08d51 Bump actions/checkout from 6.0.0 to 6.0.1 (#15005)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 12:48:24 +01:00
dependabot[bot] 0473fec367 Bump azure/trusted-signing-action from 0.5.10 to 0.5.11 (#15004)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.10 to 0.5.11.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/fc390cf8ed0f14e248a542af1d838388a47c7a7c...1d365fec12862c4aa68fcac418143d73f0cea293)

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

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

Before:

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

After:

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

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

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

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

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

    Attempted function clauses (showing 1 out of 1):

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

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

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

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

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

---------

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

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

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

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

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

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

This reverts commit a7c008f2cd.

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

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

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

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

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

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

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

Now it works:

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

    NimbleCSV is a small and fast parsing and dumping library.

    # NimbleCSV.RFC4180

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

    # NimbleCSV.Spreadsheet

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

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

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

    $ markdownlint-cli2

or automatically fix issues with

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 13:54:01 -08:00
dependabot[bot] 98b35eaa53 Bump actions/checkout from 5.0.0 to 5.0.1 (#14919)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...93cb6efe18208431cddfb8368fd83d5badbf9bfd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 13:52:53 -08:00
Austin Ziegler 5732283ada Add :formatter to ExUnit.CaptureLog options (#14914)
Mostly modified `:log_capture_on` handler for ExUnit.CaptureServer to
respect a `:formatter` option instead `Logger.default_formatter/1`.

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 08:05:00 +01:00
dependabot[bot] e9370bb023 Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#14863)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 08:04:35 +01:00
José Valim f175f5f3fa Ensure flags are passed down to umbrella children
Closes #14852.
Closes #14860.
2025-10-26 13:25:28 +01:00
José Valim a7258fe26d Ensure --no-optional-deps skips them even after they are compiled, closes #14856 2025-10-26 10:57:01 +01:00
José Valim a8000f211a Raise on invalid .app files (#14861)
Since regular expressions now have regexes,
they can end-up in .app files, and then apps
can no longer boot.

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

Before this PR:

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

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

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

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

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

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

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

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

With this PR:

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-21 12:59:34 +02:00
José Valim 5d29d61890 Simplify always true and always false bdd nodes 2025-10-21 12:51:30 +02:00
José Valim faa8d4722e Make bdd functions public to aid debugging 2025-10-21 12:51:30 +02:00
Yinameah 809b035dcc Fix typo in gen-tcp (Guide) (#14845) 2025-10-18 21:39:08 +02:00
Travis Vander Hoop 50a6f0f5e6 Update documentation for formatter's :excludes option (#14842) 2025-10-18 20:35:00 +02:00
José Valim e1772ef73a Do not spawn partitions when all dependencies are local and ok, closes #14843 2025-10-18 19:03:46 +02:00
José Valim 4e867b3775 Simplify computation of mismatched parts for error messages
Before we were doing:

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

But the second clause is:

    actual and not (actual and expected)

Which is literally the same as:

    actual and not expected

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

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

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

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

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

     it is enough to write:

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

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

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

This reverts commit 01c82022f2.

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

* Optimize Enum.into(enum, map_set)

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 20:05:09 +02:00
José Valim f9dbdb88c9 Accept any enumerable in Logger.metadata/1 2025-09-09 15:31:14 +02:00
Jean Klingler e2f6bbafaa Fix dialyzer opaqueness warnings on module attrs in OTP28 (#14755)
* Mark module attributes as generated in case they contain opaque terms

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

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

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

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

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

This fix sorts them alphabetically.

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:34:43 +02:00
Eksperimental f93919f54a ExUnit: Raise explaining what failed on invalid tags (#14707) 2025-08-08 08:43:30 +02:00
Eksperimental 7d50f43217 Rearrange Examples doc section in System.cmd/3 (#14706)
Examples showed up before "Options" were explained.
2025-08-08 07:49:19 +02:00
Steve Cohen 1f786273e6 Fix filtering documentation (#14705)
The filtering documentation implied that the msg attribute of the
logger event map could be a binary, but according to the erlang types
(https://www.erlang.org/doc/apps/kernel/logger.html#t:log_event/0) it
can't be a binary.

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

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

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

Prompt
======

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

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

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

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

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

    --baz, --no-baz

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 12:45:34 +02:00
dependabot[bot] 79a6a57234 Bump erlef/setup-beam from 1.20.3 to 1.20.4 (#14652)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.20.3 to 1.20.4.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/8e7fdef09ffa3ea54328b5000c170a3b9b20ca96...e6d7c94229049569db56a7ad5a540c051a010af9)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 13:10:44 +02:00
Jean Klingler 0be3e70037 Prevent mix test from overriding :failures_manifest_path option (#14632)
Introduced in 99be673
2025-07-08 09:00:58 +02:00
Zach Daniel e78f105f23 use simpler words in mix-and-otp guides (#14630) 2025-07-07 18:11:06 +02:00
José Valim 1fb7ca159c Fix logger docs
Closes #14628.
Closes #14629.
2025-07-06 09:46:31 +02:00
José Valim 785ffcc7cd Add TODO 2025-07-06 09:46:31 +02:00
José Valim cfc43b37d4 Add notes to the code 2025-07-06 09:46:31 +02:00
Jean Klingler 57c254f725 Increase test coverage of IEx ls helper, handle edge case and make windows consistent (#14626) 2025-07-05 17:49:19 +09:00
José Valim efcd164e38 Fix return type of phi, closes #14621 2025-07-04 09:34:28 +02:00
José Valim b29c83a42f Apply further fn optimizations and fixes (#14619)
Closes #14598
2025-07-03 15:58:17 +02:00
José Valim d7bdea55de Add required field back to struct info
Closes #14616.
Closes #14617.
Closes #14500.
2025-07-03 12:21:39 +02:00
Guillaume Duboc 824fc3bc24 Perf optimizations and inferred intersections (#14605) 2025-07-03 11:39:54 +02:00
Łukasz Samson 3e373e8a59 Handle filesystem errors in iex helpers (#14618)
`File.cd` and `File.ls` can return any posix error code
2025-07-03 10:24:19 +02:00
Michał Łępicki 0253f614b7 Fix parallel option type in Mix.Compilers.Erlang.compile/6 spec (#14615)
compile.yecc and compile.leex tasks call it with parallel: true
2025-07-01 22:34:21 +02:00
Łukasz Samson b26ae516ac Add missing erlang compiler options (#14614)
Document options on leex and yecc compilers
2025-07-01 15:53:34 +02:00
José Valim cf8689e498 Remove specs which are pass through and from private modules 2025-07-01 10:44:03 +02:00
dependabot[bot] dc1890b879 Bump erlef/setup-beam from 1.19.0 to 1.20.1 (#14612)
---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.20.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 10:20:33 +02:00
Łukasz Samson e3f7759fde Replace keyword with concrete keyword lists in specs (#14611) 2025-06-30 17:24:28 +02:00
José Valim 2fe44b747d Fix warnings on Erlang/OTP 28 2025-06-30 11:57:23 +02:00
José Valim 32ed2c38b2 Remove Regex warning until Erlang/OTP 28.1 2025-06-30 10:55:16 +02:00
José Valim ad524f528f Improve URI docs 2025-06-29 16:21:32 +02:00
José Valim 237263c446 Make sure we log all output when partition fails 2025-06-27 15:29:50 +02:00
José Valim a5d2aa88f3 Deal with undefined on :shell.whereis/0 2025-06-26 13:48:55 +02:00
José Valim b188f4d33d Do not send quoted expressions to Macro.dbg 2025-06-26 13:38:14 +02:00
José Valim 3482e4e740 Fix pry on Erlang/OTP 28 2025-06-26 13:32:51 +02:00
Art Kay 8172ff0775 Fix minor typo in the docs (#14606) 2025-06-26 07:17:16 +09:00
José Valim 274169f628 Update CHANGELOG.md 2025-06-25 20:45:16 +02:00
José Valim 08d3865fad Transform the struct update syntax into a type assertion
This transforms the struct update into a type assertion,
requiring the type system to be sure the expression has
precisely the given struct type.

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 16:14:39 -07:00
José Valim e27cd15715 Optimize empty_difference_subtype? for dynamic parts 2025-06-14 12:13:15 -07:00
Jean Klingler d59f49aa54 Mark inlined function call result as generated (#14581) 2025-06-14 19:05:31 +09:00
Tobias Pfeiffer 7ad145082b Document Map.fetch!/2 exception in its doc (#14573) 2025-06-12 10:53:06 +02:00
José Valim fb9a9e97d8 Allow captures to be reconstructed on type system pretty printing 2025-06-11 13:51:24 +02:00
José Valim 5f2633a305 Do no start listeners if --no-deps-check is given 2025-06-11 12:08:45 +02:00
José Valim e04444c42e Warn when invalid fun typespec is used 2025-06-11 12:08:45 +02:00
Jean Klingler 7da4d6570b Remove redundant memory-per-call test (#14572) 2025-06-11 16:03:44 +09:00
José Valim 5946e8bb9d Improve error message for bad/missing clauses, closes #14569 2025-06-10 14:38:30 +02:00
Joe Yates 46b5fabc63 Fix use of prefer with '-ing' (#14568) 2025-06-10 11:17:04 +02:00
José Valim bff9f3ebbc Point out module must be required before macro usage in match/guard 2025-06-10 10:13:50 +02:00
José Valim ff4adca18a Filter @compile debug_info when explicitly set to true
Closes #14567.
2025-06-10 09:58:23 +02:00
Theodor-Alexandru Irimia 954133d64c Clarify why and how to start second session for tests (#14566) 2025-06-09 22:26:53 +02:00
José Valim 319360ce9f Fix logger function link in CHANGELOG 2025-06-09 22:26:31 +02:00
Tomasz Marek Sulima 7ef6905484 Sort by call on tprof memory tests (Erlang/OTP 28) (#14565) 2025-06-09 22:25:47 +02:00
José Valim 52495ba8bc Add tests for optional subtyping 2025-06-09 13:08:12 +02:00
José Valim 514fbbaf52 Update tables 2025-06-09 12:44:56 +02:00
Michał Łępicki 34cf44587c Fix mistake in "Untracked compile-time dependencies" anti-pattern (#14563) 2025-06-09 12:07:18 +02:00
José Valim 57ed90d5b7 Also download rebar3 automatically when compiling 2025-06-08 19:13:05 +02:00
José Valim 09dcbf7566 Introduce deprecations scheduled for Elixir v1.20 2025-06-07 21:55:06 +02:00
José Valim c001cbdd62 Start v1.20 2025-06-07 20:21:32 +02:00
370 changed files with 25516 additions and 10732 deletions
-40
View File
@@ -1,40 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: CI for Markdown content
on:
push:
branches:
- "main"
paths:
- "lib/**/*.md"
pull_request:
paths:
- "lib/**/*.md"
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
name: Lint Markdown content
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+57 -78
View File
@@ -1,15 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: CI
on:
push:
paths-ignore:
- "lib/**/*.md"
pull_request:
paths-ignore:
- "lib/**/*.md"
workflow_dispatch:
env:
ELIXIR_ASSERT_TIMEOUT: 2000
@@ -21,19 +19,19 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- otp_version: "28.0"
- otp_version: "28.1"
deterministic: true
- otp_version: "28.0"
- otp_version: "28.1"
erlc_opts: "warnings_as_errors"
docs: true
coverage: true
- otp_version: "28.0"
otp_latest: true
erlc_opts: "warnings_as_errors"
- otp_version: "27.3"
erlc_opts: "warnings_as_errors"
- otp_version: "27.0"
@@ -43,43 +41,46 @@ jobs:
development: true
- otp_version: maint
development: true
runs-on: ubuntu-24.04
# Earlier Erlang/OTP versions ignored compiler directives
# when using warnings_as_errors. So we only set ERLC_OPTS
# from Erlang/OTP 27+.
env:
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
with:
otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir
run: |
make compile
echo "$PWD/bin" >> $GITHUB_PATH
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
continue-on-error: ${{ matrix.development }}
continue-on-error: ${{ matrix.development == true }}
- name: Elixir test suite
run: make test_elixir
continue-on-error: ${{ matrix.development }}
continue-on-error: ${{ matrix.development == true }}
env:
COVER: "${{ matrix.coverage }}"
- name: "Calculate Coverage"
run: make cover | tee "$GITHUB_STEP_SUMMARY"
if: "${{ matrix.coverage }}"
- name: Build docs (ExDoc main)
if: ${{ matrix.otp_latest }}
if: ${{ matrix.docs }}
run: |
cd ..
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
@@ -88,87 +89,65 @@ jobs:
cd ../elixir/
git fetch --tags
DOCS_OPTIONS="--warnings-as-errors" make docs
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
# Recompile System without .git
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
- name: "Calculate Coverage"
if: ${{ matrix.coverage }}
run: make cover | tee "$GITHUB_STEP_SUMMARY"
- name: "Upload Coverage Artifact"
if: "${{ matrix.coverage }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: TestCoverage
path: cover/*
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: taskset 1 make check_reproducible
- name: Check git is not required
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
cd lib/elixir
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
test_windows:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
runs-on: windows-2022
strategy:
matrix:
otp_version: ["26.2", "27.3", "28.0"]
runs-on: windows-2022
otp_version:
- "28.1"
- "27.3"
- "26.2"
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
with:
otp-version: ${{ matrix.otp_version }}
- name: Compile Elixir
run: |
Remove-Item -Recurse -Force '.git'
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
- name: Elixir test suite
run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
make test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- name: Install Shellcheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Check POSIX-compliant
run: |
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
license_compliance:
name: Check Licence Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Run OSS Review Toolkit"
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
+39
View File
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: License Compliance
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
license_compliance:
name: Check License Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run OSS Review Toolkit
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
+39
View File
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Markdown Content
on:
push:
branches:
- "main"
paths: &paths-filter
- "**/*.md"
- .github/workflows/markdown.yml
- .markdownlint-cli2.jsonc
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
lint:
name: Lint Markdown content
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
+11 -1
View File
@@ -62,6 +62,16 @@ runs:
# Override Default Evaluator Rules
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
# Add Package Configurations
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
for FILE in .ort/package-configurations/*.yml; do
COMPONENT="$(basename "$FILE")"
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
sed -i -E \
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
done
# Set Version in SPDX & Config
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
@@ -80,7 +90,7 @@ runs:
id: ort
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with:
image: ghcr.io/oss-review-toolkit/ort-minimal:54.0.0
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >-
labels,
cache-dependencies,
+51
View File
@@ -0,0 +1,51 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: POSIX Compliance
on:
push:
paths: &paths-filter
- .github/workflows/posix_compliance.yml
- bin/elixir
- bin/elixirc
- bin/iex
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
check_posix_compliance:
name: Check POSIX compliance
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install ShellCheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Run ShellCheck on bin/ dir
run: |
shellcheck -e SC2039,2086 bin/elixir && \
echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && \
echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && \
echo "bin/iex is POSIX compliant"
+73 -54
View File
@@ -1,16 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Release
name: Releases
on:
push:
branches:
- main
- v*.*
tags:
- v*
workflow_dispatch:
env:
ELIXIR_OPTS: "--warnings-as-errors"
LANG: C.UTF-8
@@ -20,11 +23,15 @@ permissions:
jobs:
create_draft_release:
runs-on: ubuntu-22.04
name: Create draft release
runs-on: ubuntu-24.04
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
@@ -36,10 +43,8 @@ jobs:
--draft \
${{ github.ref_name }}
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
if: github.ref_type == 'branch'
with:
fetch-depth: 50
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
@@ -58,7 +63,8 @@ jobs:
git push origin $ref_name --force
build:
name: "Build Elixir"
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
runs-on: ubuntu-24.04
strategy:
fail-fast: true
@@ -66,18 +72,16 @@ jobs:
include:
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
- otp: 28
otp_version: "28.0"
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
@@ -92,27 +96,29 @@ jobs:
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: matrix.build_docs
with:
name: Docs
path: Docs.zip*
sign:
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
@@ -122,34 +128,33 @@ jobs:
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
steps:
- name: "Download build"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
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
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
# AZURE_TENANT_ID and AZURE_CLIENT_ID should stay the same,
# but AZURE_CLIENT_SECRET has expiration date. When it expires go to
# App Registrations / <app> / Certificates & secrets,
# click (+) New client secret, note the "Value" (not "Secret ID")
# and update it:
#
# $ gh --repo elixir-lang/elixir secret set AZURE_CLIENT_SECRET
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: trusted-signing-elixir
certificate-profile-name: Elixir
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
@@ -173,17 +178,15 @@ jobs:
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
sbom:
name: Generate SBoM
needs: [build, sign]
runs-on: ubuntu-24.04
permissions:
@@ -199,11 +202,11 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -218,7 +221,7 @@ jobs:
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -246,7 +249,7 @@ jobs:
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
- name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: "SBoM"
path: |
@@ -256,25 +259,26 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: "Attestations"
path: "attestations/*.sigstore"
upload-release:
name: Upload release
needs: [create_draft_release, build, sign, sbom]
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
- name: Upload Pre-built
- name: Upload Pre-build
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -301,20 +305,26 @@ jobs:
bom.*
upload-builds-hex-pm:
name: Upload builds to hex.pm
runs-on: ubuntu-24.04
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
OTP_GENERIC_VERSION: "25"
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -327,6 +337,8 @@ jobs:
run: |
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/}
@@ -336,7 +348,7 @@ jobs:
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
@@ -369,6 +381,8 @@ jobs:
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
@@ -379,7 +393,7 @@ jobs:
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
@@ -418,3 +432,8 @@ jobs:
for key in $(cat purge_keys.txt); do
purge "${key}"
done
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Notify
name: Release Notifications
on:
release:
@@ -15,14 +15,15 @@ jobs:
notify:
runs-on: ubuntu-latest
name: Notify
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
with:
otp-version: "27.3"
elixir-version: "1.18.3"
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
+11 -3
View File
@@ -1,33 +1,41 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: "Release pre built"
description: "Builds elixir release, ExDoc and generates docs"
name: Release Pre-build
description: "Builds Elixir release, ExDoc and generates docs"
inputs:
otp:
description: "The major OTP version"
otp_version:
description: "The exact OTP version (major.minor[.patch])"
build_docs:
description: "If docs have to be built or not"
description: "Whether docs have to be built"
runs:
using: "composite"
steps:
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
with:
otp-version: ${{ inputs.otp_version }}
version-type: strict
- name: Build Elixir Release
shell: bash
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS
shell: bash
run: |
sudo apt update
sudo apt install -y nsis
- name: Build Elixir Windows Installer
shell: bash
run: |
+3 -2
View File
@@ -10,9 +10,10 @@
/lib/elixir/test/ebin/
/man/elixir.1
/man/iex.1
/Docs-v*.zip
/Precompiled-v*.zip
/Docs.zip
/Precompiled.zip
/.eunit
.elixir.plt
erl_crash.dump
/cover/
.tool-versions
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
"globs": [
"**/*.md"
],
"ignores": [
".git/**"
],
"gitignore": true,
"config": {
// Consecutive header levels (h1 -> h2 -> h3).
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content.
"MD024": {
// Duplication is allowed for headings with different parents.
"siblings_only": true
},
// Trailing punctuation in heading.
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
// the default values.
"MD026": {
"punctuation": ".,;:。,;:!"
},
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false,
// Our tables are too large to align.
"MD060": false
}
}
-45
View File
@@ -1,45 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false
}
+56 -83
View File
@@ -3,18 +3,6 @@
excludes:
paths:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "man/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
@@ -25,8 +13,64 @@ excludes:
reason: "BUILD_TOOL_OF"
comment: "Documentation"
# Unfortunately we'll have to repeat all package level excludes here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
- pattern: "lib/*/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "lib/*/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/*/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
curations:
license_findings:
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
# Unfortunately we'll have to repeat all package level license curations here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
# Test Fixtures
- path: "lib/*/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
@@ -39,13 +83,6 @@ curations:
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
@@ -54,26 +91,11 @@ curations:
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
@@ -89,57 +111,8 @@ curations:
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
packages:
- id: "SpdxDocumentFile:The Elixir Team:elixir-lang:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
- id: "SpdxDocumentFile:The Elixir Team:eex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:elixir:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:exunit:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:iex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:logger:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:mix:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
+8 -1
View File
@@ -3,6 +3,7 @@
ort:
enableRepositoryPackageCurations: true
enableRepositoryPackageConfigurations: true
scanner:
skipConcluded: false
@@ -11,4 +12,10 @@ ort:
analyzer:
allowDynamicVersions: true
enabledPackageManagers: [SpdxDocumentFile]
skipExcluded: true
reporter:
reporters:
SpdxDocument:
options:
creationInfoOrganization: The Elixir Team
documentName: "Elixir Source SPDX Document"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:eex:"
path_excludes:
- pattern: "lib/eex/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+60
View File
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:elixir:"
path_excludes:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/elixir/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:exunit:"
path_excludes:
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/ex_unit/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:logger:"
path_excludes:
- pattern: "lib/logger/test/**/*"
reason: "TEST_OF"
comment: "Tests"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:mix:"
path_excludes:
- pattern: "lib/mix/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+136 -246
View File
@@ -4,11 +4,13 @@
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.19
# Changelog for Elixir v1.20
## Type system improvements
### More type inference
This release includes type inference of all constructs.
### Type inference of function calls
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
@@ -30,294 +32,182 @@ end
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
### Type checking of protocol dispatch and implementations
### Type inference of guards
This release also adds type checking when dispatching and implementing protocols.
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
This release also performs inference of guards! Let's see some examples:
```elixir
defmodule Example do
def my_code(first..last//step = range) do
"hello #{range}"
def example(x, y) when is_list(x) and is_integer(y)
```
The code above correctly infers `x` is a list and `y` is an integer.
```elixir
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
```
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
```elixir
def example(x) when is_map_key(x, :foo)
```
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
```elixir
def example(x) when not is_map_key(x, :foo)
```
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
You can also have expressions that assert on the size of data structures:
```elixir
def example(x) when tuple_size(x) < 3
```
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
### Complete typing of maps keys
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
```elixir
%{123 => "hello", 456.0 => :ok}
```
will have the type:
```elixir
%{integer() => binary(), float() => :ok}
```
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
```elixir
%{integer() => integer(), root: integer()}
```
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
### Typing of map operations
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
```elixir
Map.put(map, :key, 123)
#=> returns type %{..., key: integer()}
Map.delete(map, :key)
#=> returns type %{..., key: not_set()}
```
As you can see, we track when keys are set and also when they are removed.
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
```elixir
Map.replace(map, :key, 123)
#=> returns type %{..., key: if_set(integer())}
```
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
```elixir
defmodule User do
def name(map), do: Map.fetch!(map, :name)
end
defmodule CallsUser do
def calls_name do
User.name(%{})
end
end
```
the above emits the following warnings:
The code above has a type violation, which is now caught by the type system:
```
warning: incompatible value given to string interpolation:
```text
warning: incompatible types given to User.name/1:
data
it has type:
%Range{first: term(), last: term(), step: term()}
but expected a type that implements the String.Chars protocol, it must be one of:
dynamic(
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
%Version.Requirement{}
) or atom() or binary() or float() or integer() or list(term())
```
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
```elixir
defmodule Example do
def my_code(%Date{} = date) do
for(x <- date, do: x)
end
end
```
will emit:
```
warning: incompatible value given to for-comprehension:
x <- date
it has type:
%Date{year: term(), month: term(), day: term(), calendar: term()}
but expected a type that implements the Enumerable protocol, it must be one of:
dynamic(
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
) or fun() or list(term()) or non_struct_map()
```
### Type checking and inference of anonymous functions
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
```elixir
defmodule Example do
def run do
fun = fn %{} -> :map end
fun.("hello")
end
end
```
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
```
warning: incompatible types given on function application:
fun.("hello")
User.name(%{})
given types:
binary()
%{name: not_set()}
but function has type:
but expected one of:
(dynamic(map()) -> :map)
dynamic(%{..., name: term()})
typing violation found at:
│
6 │ fun.("hello")
│ ~
16 │ User.name(%{})
│ ~
│
└─ mod.exs:6:8: Example.run/0
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
```
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
### Acknowledgements
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
## Faster compile times in large projects
## v1.20.0-rc.1 (2026-01-13)
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
### 1. Bug fixes
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU cores.
#### Elixir
### Code loading bottlenecks
* [Kernel] Improve the performance of the type system when working with large unions of open maps
* [Kernel] Do not crash on map types with struct keys when performing type operations
* [Kernel] Mark the outcome of bitstring types as dynamic
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
The only potential regression in this approach happens if you have a module, which is used at compile time and defines an `@on_load` callback (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invokes another modules within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
@on_load :init
def init do
MyLib.AnotherModule.do_something()
end
def something_else do
...
end
end
MyLib.SomeModule.something_else()
```
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
## Improved pretty printing algorithm
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
```elixir
[
[1, 2, 3],
[1, 2, ...],
[1, ...],
[...],
...
]
```
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on actually filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
```elixir
[
[1, 2, 3],
...
]
```
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
## OpenChain certification
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
* Each release is attested along with the Source SBoM.
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
## v1.19.0-dev
## v1.20.0-rc.0 (2026-01-09)
### 1. Enhancements
#### Elixir
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
* [Code.Fragment] Add `Code.Fragment.lines/1`
* [Enum] Provide more information on `Enum.OutOfBoundsError`
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Allow controlling which applications are used during inference
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [String] Add `String.count/2` to count occurrences of a pattern
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Enum] Add `Enum.min_max` sorter
* [Integer] Add `Integer.ceil_div/2`
* [IO] Add `IO.iodata_empty?/1`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Warn on unused requires
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
#### ExUnit
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
#### IEx
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
* [mix deps] Support filtering `mix deps` output
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix test] Add `mix test --dry-run`
### 2. Bug fixes
### 2. Hard deprecations
#### Elixir
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
#### Mix
* [mix cmd] Preserve argument quoting in subcommands
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
* [mix test] Preserve files with no longer filter on `mix test`
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
### 4. Hard deprecations
#### Elixir
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}` is deprecated in favor of pattern matching on the struct when the variable is defined and then using the map update syntax `%{uri | path: "/foo/bar"}`. Thanks to the type system, pattern matching on structs can find more errors, more reliably
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
#### Logger
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
#### Mix
## v1.19
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
## v1.18
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
+5 -5
View File
@@ -6,7 +6,7 @@
# Code of Conduct
Contact: elixir-lang-conduct@googlegroups.com
Contact: <elixir-lang-conduct@googlegroups.com>
## Why have a Code of Conduct?
@@ -51,15 +51,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
* The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
* The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
+37 -29
View File
@@ -78,6 +78,7 @@ introduced behavior, especially for bug fixes and major changes:
*fails* before your change and *passes* afterward. This makes it easier to
confirm that the fix addresses the underlying issue and helps prevent
regressions in the future.
* **New Features or Major Changes:** If you are adding a new feature or making
major changes to existing functionality, please add tests that cover the
major parts of that functionality. Aim to have the best code coverage possible.
@@ -88,7 +89,9 @@ We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes
@@ -121,30 +124,34 @@ Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
guidelines on licensing and compliance. Below is a summary of the key points
affecting **all external contributors**:
- Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
- SPDX License Headers: With the exception of approved test fixture files,
all new or modified files in a pull request must include correct SPDX
headers. If you are creating a new file under the `Apache-2.0` license, for
instance, please use:
* Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
* SPDX License Headers: With the exception of approved test fixture files,
all new or modified files in a pull request must include correct SPDX
headers. If you are creating a new file under the `Apache-2.0` license, for
instance, please use:
```elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
```
- No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
- Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
- Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
- Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```
* No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
* Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
* Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
* Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```text
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
@@ -171,25 +178,26 @@ affecting **all external contributors**:
involved.
```
See http://developercertificate.org/ for a copy of the Developer Certificate
See <https://developercertificate.org/> for a copy of the Developer Certificate
of Origin license.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
is installed and built alongside Elixir.
After cloning and compiling Elixir, run:
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
```
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 go back to Elixir's root directory and run:
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
```sh
make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
+3 -1
View File
@@ -107,8 +107,10 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
fi
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
$(Q) $(GENERATE_APP) $(VERSION)
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
$(Q) $(GENERATE_APP) $(VERSION)
unicode: $(UNICODE)
+10 -13
View File
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
## 2. Scope
This policy applies to the Elixir Programming language, located at
https://github.com/elixir-lang/elixir. It covers every file, and contribution
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
made, including documentation and any associated assets.
## 3. Licensing
@@ -29,18 +29,19 @@ All code released by the Elixir team is licensed under the
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
licenses are recognized as permissible in this project:
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
These licenses are considered acceptable for any files or code that form part of
an Elixir repository. If a contribution requires a different license, it must
either be rejected or prompt an update to this policy.
## 4. Contributing to Elixir Projects
## 4. Contributing to the Elixir repository
Any code contributed to Elixir repositories must fall under one of the accepted
Any code contributed to the Elixir repository must fall under one of the accepted
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
other license will be rejected unless this policy is formally revised to include
that license. All files except those specifically exempted (e.g., certain test
@@ -51,13 +52,9 @@ configuration and undergo review.
Contributions must not introduce executable binary files into the codebase.
Every Elixir project within the organization will have an automated GitHub
Action to enforce these rules. This mechanism aids in detecting non-compliant
licenses or files early in the review process.
## 5. Preservation of Copyright and License Information
Any third-party code incorporated into Elixir projects must retain original
Any third-party code incorporated into the Elixir repository must retain original
copyright and license headers. If no such headers exist in the source, they must
be added. This practice ensures that original authors receive proper credit and
that the licensing lineage is preserved.
@@ -165,4 +162,4 @@ necessary, by the EEF CISO. Any significant changes will be communicated to
contributors and made publicly available.
*Effective Date: 2025-02-20*
*Last Reviewed: 2025-02-20*
*Last Reviewed: 2025-11-20*
+1 -1
View File
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
comment and we can always reopen the issue.
By keeping the overall issues tracker tidy and organized, the community
can easily peak at what is coming in new releases and also get involved
can easily peek at what is coming in new releases and also get involved
by commenting on existing issues and submitting pull requests. Please
remember to keep the tone positive and be kind! For more information,
see the [Code of Conduct][1].
+4 -10
View File
@@ -8,15 +8,9 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
- If this release addresses any publicly known security vulnerabilities with
assigned CVEs, add a "Security" section to `CHANGELOG.md`. For example:
```md
## Security
- Fixed CVE-2025-00000: Description of the vulnerability
```
3. Update "Compatibility and Deprecations" if a new OTP version is supported
@@ -30,11 +24,11 @@
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
## Creating a new vMAJOR.MINOR branch (usually before first rc)
### In the new branch
1. Comment the `CANONICAL=` in /Makefile
1. Comment out `CANONICAL := main/` in /Makefile
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
@@ -42,7 +36,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat
2. Start new /CHANGELOG.md
+4 -4
View File
@@ -12,16 +12,16 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.19 | Development
1.18 | Bug fixes and security patches
1.20 | Development
1.19 | Bug fixes and security patches
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
## Announcements
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
+1 -1
View File
@@ -1 +1 @@
1.19.0-dev
1.20.0-rc.1
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.0-dev
ELIXIR_VERSION=1.20.0-rc.1
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.0-dev
set ELIXIR_VERSION=1.20.0-rc.1
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+23 -6
View File
@@ -118,6 +118,19 @@ defmodule EEx do
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
| {:eof, metadata}
@type tokenize_opt ::
{:file, binary()}
| {:line, line}
| {:column, column}
| {:indentation, non_neg_integer}
| {:trim, boolean()}
@type compile_opt ::
tokenize_opt
| {:engine, module()}
| {:parser_options, Code.parser_opts()}
| {atom(), term()}
@doc """
Generates a function definition from the given string.
@@ -128,6 +141,7 @@ defmodule EEx do
template.
The supported `options` are described [in the module docs](#module-options).
Additional options are passed to the underlying engine.
## Examples
@@ -220,9 +234,11 @@ defmodule EEx do
"3"
"""
@spec compile_string(String.t(), keyword) :: Macro.t()
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
case tokenize(source, options) do
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
case tokenize(source, tokenize_opts) do
{:ok, tokens} ->
EEx.Compiler.compile(tokens, source, options)
@@ -259,7 +275,7 @@ defmodule EEx do
#=> "3"
"""
@spec compile_file(Path.t(), keyword) :: Macro.t()
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
def compile_file(filename, options \\ []) when is_list(options) do
filename = IO.chardata_to_string(filename)
options = Keyword.merge([file: filename, line: 1], options)
@@ -277,7 +293,7 @@ defmodule EEx do
"foo baz"
"""
@spec eval_string(String.t(), keyword, keyword) :: String.t()
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options)
@@ -299,7 +315,7 @@ defmodule EEx do
#=> "foo baz"
"""
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename)
@@ -339,7 +355,7 @@ defmodule EEx do
Note new tokens may be added in the future.
"""
@doc since: "1.14.0"
@spec tokenize([char()] | String.t(), opts :: keyword) ::
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
{:ok, [token()]} | {:error, String.t(), metadata()}
def tokenize(contents, opts \\ []) do
EEx.Compiler.tokenize(contents, opts)
@@ -348,6 +364,7 @@ defmodule EEx do
### Helpers
defp do_eval(compiled, bindings, options) do
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
{result, _} = Code.eval_quoted(compiled, bindings, options)
result
end
+2 -2
View File
@@ -96,7 +96,7 @@ defmodule EEx.Compiler do
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
"please remove \"#{marker}\""
:elixir_errors.erl_warn({line, column}, state.file, message)
IO.warn(message, file: state.file, line: line, column: column)
~c""
else
marker
@@ -373,7 +373,7 @@ defmodule EEx.Compiler do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
+4
View File
@@ -17,6 +17,10 @@ defmodule EEx.Engine do
@doc """
Called at the beginning of every template.
It receives the options during compilation, including the
ones managed by EEx, such as `:line` and `:file`, as well
as custom engine options.
It must return the initial state.
"""
@callback init(opts :: keyword) :: state
+1 -11
View File
@@ -873,11 +873,6 @@ defmodule Access do
...> end)
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.filter(5)])
** (FunctionClauseError) no function clause matching in Access.filter/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
@@ -891,7 +886,7 @@ defmodule Access do
end
defp filter(:get, data, func, next) when is_list(data) do
data |> Enum.filter(func) |> Enum.map(next)
for elem <- data, func.(elem), do: next.(elem)
end
defp filter(:get_and_update, data, func, next) when is_list(data) do
@@ -1154,11 +1149,6 @@ defmodule Access do
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
+11 -10
View File
@@ -507,7 +507,7 @@ defmodule Application do
of all loaded applications. Returns `nil` if
the module is not listed in any application spec.
"""
@spec get_application(atom) :: atom | nil
@spec get_application(module) :: app | nil
def get_application(module) when is_atom(module) do
case :application.get_application(module) do
{:ok, app} -> app
@@ -696,7 +696,7 @@ defmodule Application do
config :my_app, Databases.RepoTwo,
# Another database configuration (for the same OTP app)
ip: "localhost",
port: 20717
port: 20_717
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
@@ -814,7 +814,7 @@ defmodule Application do
stick after the application is loaded and also on application reload.
"""
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
def put_env(app, key, value, opts \\ []) when is_atom(app) do
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do
maybe_warn_on_app_env_key(app, key)
:application.set_env(app, key, value, opts)
end
@@ -856,7 +856,7 @@ defmodule Application do
It receives the same options as `put_env/4`. Returns `:ok`.
"""
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
def delete_env(app, key, opts \\ []) when is_atom(app) do
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do
maybe_warn_on_app_env_key(app, key)
:application.unset_env(app, key, opts)
end
@@ -903,13 +903,13 @@ defmodule Application do
@doc """
Ensures the given `app` or `apps` and their child applications are started.
The second argument is either the `t:restart_type/1` (for consistency with
The second argument is either the `t:restart_type/0` (for consistency with
`start/2`) or a keyword list.
## Options
* `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
`:permanent`, or `:transient`. See `t:restart_type/0` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`).
@@ -921,11 +921,11 @@ defmodule Application do
{:ok, [app]} | {:error, term}
def ensure_all_started(app_or_apps, type_or_opts \\ [])
def ensure_all_started(app, type) when is_atom(type) do
ensure_all_started(app, type: type)
def ensure_all_started(app_or_apps, type) when is_atom(type) do
ensure_all_started(app_or_apps, type: type)
end
def ensure_all_started(app, opts) when is_atom(app) do
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do
ensure_all_started([app], opts)
end
@@ -1056,7 +1056,8 @@ defmodule Application do
Returns a list with information about the applications which are currently running.
"""
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
def started_applications(timeout \\ 5000) do
def started_applications(timeout \\ 5000)
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
:application.which_applications(timeout)
end
+17 -1
View File
@@ -162,6 +162,22 @@ defmodule Calendar do
"""
@type time_zone_database :: module()
@typedoc """
Options for formatting dates and times with `strftime/3`.
"""
@type strftime_opts :: [
preferred_datetime: String.t(),
preferred_date: String.t(),
preferred_time: String.t(),
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_month_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_day_of_week_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
]
@doc """
Returns how many days there are in the given month of the given year.
"""
@@ -617,7 +633,7 @@ defmodule Calendar do
"""
@doc since: "1.11.0"
@spec strftime(map(), String.t(), keyword()) :: String.t()
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse(
+13 -8
View File
@@ -53,7 +53,7 @@ defmodule Date do
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
14716
iex> Date.add(~D[1970-01-01], 14716)
iex> Date.add(~D[1970-01-01], 14_716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
@@ -321,7 +321,7 @@ defmodule Date do
@doc """
Converts the given date to a string according to its calendar.
### Examples
## Examples
iex> Date.to_string(~D[2000-02-28])
"2000-02-28"
@@ -399,7 +399,7 @@ defmodule Date do
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise an `ArgumentError`.
### Examples
## Examples
iex> Date.to_iso8601(~D[2000-02-28])
"2000-02-28"
@@ -633,7 +633,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
@@ -667,7 +667,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
@@ -691,10 +691,15 @@ defmodule Date do
@doc """
Adds the number of days to the given `date`.
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
>
> `add/2` always considers a day to be measured according to the
> `Calendar.ISO`.
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
The days are counted as Gregorian days, independent of the underlying
calendar. The date is returned in the same calendar as it was given in.
## Examples
+1 -1
View File
@@ -95,7 +95,7 @@ defmodule Date.Range do
[date_from_iso_days(current, calendar)]
end
defp slice(current, step, remaining, calendar) do
defp slice(current, step, remaining, calendar) when remaining > 1 do
[
date_from_iso_days(current, calendar)
| slice(current + step, step, remaining - 1, calendar)
+44 -23
View File
@@ -1046,7 +1046,7 @@ defmodule DateTime do
its abbreviation, which means information is lost when converting to such
format.
### Examples
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1390,7 +1390,7 @@ defmodule DateTime do
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
### Examples
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1611,32 +1611,45 @@ defmodule DateTime do
@doc """
Adds a specified amount of time to a `DateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always considers the unit to be computed according
to the `Calendar.ISO`.
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
This function relies on a contiguous representation of time,
ignoring the wall time and timezone changes. For example, if you add
one day when there are summer time/daylight saving time changes,
it will also change the time forward or backward by one hour,
so the elapsed time is precisely 24 hours. Similarly, adding just
a few seconds to a datetime just before "spring forward" can cause
wall time to increase by more than an hour.
ignoring timezone changes. For example, if you add one day when there
are summer time/daylight saving time changes, it will also change the
time forward or backward by one hour, so the elapsed time is precisely
24 hours. Similarly, adding just a few seconds to a datetime just before
"spring forward" can cause wall time to increase by more than an hour.
While this means this function is precise in terms of elapsed time,
its result may be misleading in certain use cases. For example, if a
its result may be confusing in certain use cases. For example, if a
user requests a meeting to happen every day at 15:00 and you use this
function to compute all future meetings by adding day after day, this
function may change the meeting time to 14:00 or 16:00 if there are
changes to the current timezone. Computing of recurring datetimes is
not currently supported in Elixir's standard library but it is available
by third-party libraries.
changes to the current timezone.
### Examples
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can add to
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## Examples
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
@@ -1664,8 +1677,6 @@ defmodule DateTime do
iex> result.microsecond
{21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
"""
@doc since: "1.8.0"
@spec add(
@@ -1739,7 +1750,7 @@ defmodule DateTime do
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
On the other hand, time zones that observe "Daylight Saving Time"
Consequently, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
@@ -1751,12 +1762,22 @@ defmodule DateTime do
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## ISO calendar considerations
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
@@ -1922,7 +1943,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -1969,7 +1990,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
+17
View File
@@ -161,6 +161,22 @@ defmodule Duration do
"""
@type duration :: t | [unit_pair]
@typedoc """
Options for `Duration.to_string/2`.
"""
@type to_string_opts :: [
units: [
year: String.t(),
month: String.t(),
week: String.t(),
day: String.t(),
hour: String.t(),
minute: String.t(),
second: String.t()
],
separator: String.t()
]
@microseconds_per_second 1_000_000
@doc """
@@ -436,6 +452,7 @@ defmodule Duration do
"""
@doc since: "1.18.0"
@spec to_string(t, to_string_opts) :: String.t()
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
+78 -151
View File
@@ -182,7 +182,7 @@ defmodule Calendar.ISO do
@type day_of_year :: 1..366
@type quarter_of_year :: 1..4
@type year_of_era :: {1..10000, era}
@type year_of_era :: {1..10_000, era}
@seconds_per_minute 60
@seconds_per_hour 60 * 60
@@ -196,14 +196,28 @@ defmodule Calendar.ISO do
@ext_date_sep ?-
@ext_time_sep ?:
@days_per_nonleap_year 365
@days_per_leap_year 366
# The ISO epoch starts, in this implementation,
# with ~D[0000-01-01]. Era "1" starts
# on ~D[0001-01-01] which is 366 days later.
@iso_epoch 366
# Constants for date calculations using 400-year era cycles.
# The algorithm uses a March-based year where March 1 is day 0.
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
@days_per_year 365
@years_per_era 400
@days_per_era @years_per_era * @days_per_year + 97
@days_per_4_years 4 * @days_per_year
@days_per_100_years 100 * @days_per_year + 24
@march_1_offset 31 + 29
@unix_epoch_days 719_528
# Month calculation constants: in a March-based year, each 5-month
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
@days_per_5_months 153
@months_per_cycle 5
[match_basic_date, match_ext_date, guard_date, read_date] =
quote do
[
@@ -652,7 +666,7 @@ defmodule Calendar.ISO do
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
{{year, month, day}, {hour, minute, second, _}} =
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
{0, day_fraction} ->
{{year, month, day}, time_from_day_fraction(day_fraction)}
@@ -784,13 +798,13 @@ defmodule Calendar.ISO do
## Examples
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86_400}})
{0, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86_400}})
{2000, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43_200, 86_400}})
{2000, 1, 1, 12, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86_400_000_000}})
{-1, 1, 1, 0, 0, 0, {0, 6}}
"""
@@ -878,28 +892,49 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days(0, 1, 1) do
0
end
def date_to_iso_days(1970, 1, 1) do
719_528
end
def date_to_iso_days(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)
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
1
y = if month <= 2, do: year - 1, else: year
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
year_of_era = y - era * @years_per_era
month_prime = if month > 2, do: month - 3, else: month + 9
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
day_of_era =
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
era * @days_per_era + day_of_era + @march_1_offset
end
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
{year, month, day_in_month + 1}
z = days - @march_1_offset
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
day_of_era = z - era * @days_per_era
year_of_era =
div(
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
div(day_of_era, @days_per_era - 1),
@days_per_year
)
day_of_year =
day_of_era -
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
year = year_of_era + era * @years_per_era
year = if month <= 2, do: year + 1, else: year
{year, month, day}
end
defp div_rem(int1, int2) do
@@ -913,6 +948,9 @@ defmodule Calendar.ISO do
end
end
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
@doc """
Returns how many days there are in the given year-month.
@@ -1133,7 +1171,7 @@ defmodule Calendar.ISO do
"""
@doc since: "1.8.0"
@spec year_of_era(year) :: {1..10000, era}
@spec year_of_era(year) :: {1..10_000, era}
def year_of_era(year) when is_year_CE(year), do: {year, 1}
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
@@ -1159,7 +1197,7 @@ defmodule Calendar.ISO do
"""
@doc since: "1.13.0"
@impl true
@spec year_of_era(year, month, day) :: {1..10000, era}
@spec year_of_era(year, month, day) :: {1..10_000, era}
def year_of_era(year, _month, _day), do: year_of_era(year)
@doc """
@@ -1704,11 +1742,11 @@ defmodule Calendar.ISO do
## Examples
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86400000000}})
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86_400_000_000}})
{0, {0, 86400000000}}
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {43200000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {43_200_000_000, 86_400_000_000}})
{730485, {0, 86400000000}}
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {46800000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {46_800_000_000, 86_400_000_000}})
{730485, {0, 86400000000}}
"""
@@ -1724,11 +1762,11 @@ defmodule Calendar.ISO do
## Examples
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86400000000}})
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86_400_000_000}})
{0, {86399999999, 86400000000}}
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {43200000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {43_200_000_000, 86_400_000_000}})
{730485, {86399999999, 86400000000}}
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {46800000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {46_800_000_000, 86_400_000_000}})
{730485, {86399999999, 86400000000}}
"""
@@ -1848,7 +1886,7 @@ defmodule Calendar.ISO do
months_in_year = 12
total_months = year * months_in_year + month + months - 1
new_year = Integer.floor_div(total_months, months_in_year)
new_year = floor_div_positive_divisor(total_months, months_in_year)
new_month =
case rem(total_months, months_in_year) + 1 do
@@ -1888,7 +1926,7 @@ defmodule Calendar.ISO do
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86400, :second, unit)
ppd = System.convert_time_unit(86_400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
@@ -1937,7 +1975,7 @@ defmodule Calendar.ISO do
}) do
[
month: year * 12 + month,
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
@@ -1971,7 +2009,7 @@ defmodule Calendar.ISO do
if total in @unix_range_microseconds do
microseconds = Integer.mod(total, @microseconds_per_second)
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
precision = precision_for_unit(unit)
{date, time} = iso_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}}
@@ -2093,9 +2131,12 @@ defmodule Calendar.ISO do
end
end
# Note that this function does not add the extra leap day for a leap year.
# If you want to add that leap day when appropriate,
# add the result of leap_day_offset/2 to the result of days_before_month/1.
defp leap_day_offset(_year, month) when month < 3, do: 0
defp leap_day_offset(year, _month) do
if leap_year?(year), do: 1, else: 0
end
defp days_before_month(1), do: 0
defp days_before_month(2), do: 31
defp days_before_month(3), do: 59
@@ -2109,120 +2150,6 @@ defmodule Calendar.ISO do
defp days_before_month(11), do: 304
defp days_before_month(12), do: 334
defp leap_day_offset(_year, month) when month < 3, do: 0
defp leap_day_offset(year, _month) do
if leap_year?(year), do: 1, else: 0
end
defp days_to_year(days) when days < 0 do
year_estimate = -div(-days, @days_per_nonleap_year) - 1
{year, days_before_year} =
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
leap_year_pad = if leap_year?(year), do: 1, else: 0
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
end
defp days_to_year(days) do
year_estimate = div(days, @days_per_nonleap_year)
{year, days_before_year} =
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
{year, days - days_before_year}
end
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
end
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
end
defp days_to_year(year, _days1, days2) do
{year, days2}
end
defp days_to_end_of_epoch(year) when year < 0 do
previous_year = year + 1
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
previous_year * @days_per_nonleap_year
end
defp days_in_previous_years(0), do: 0
# A concise version of the algorithm would use floor_div instead of div.
# However, floor_div would check the operands on every operation.
# We optimize this by providing a positive and negative version of each algorithm.
defp days_in_previous_years(year) when year > 0 do
previous_year = year - 1
div(previous_year, 4) - div(previous_year, 100) +
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
defp days_in_previous_years(year) when year < 0 do
previous_year = year - 1
div(year, 4) - div(year, 100) +
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
# Note that 0 is the first day of the month.
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
{1, day_of_year}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
{2, day_of_year - 31}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
{3, day_of_year - (59 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
{4, day_of_year - (90 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
{5, day_of_year - (120 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
{6, day_of_year - (151 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
{7, day_of_year - (181 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
{8, day_of_year - (212 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
{9, day_of_year - (243 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
{10, day_of_year - (273 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
{11, day_of_year - (304 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) do
{12, day_of_year - (334 + extra_day)}
end
defp iso_seconds_to_datetime(seconds) do
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
+16 -11
View File
@@ -391,13 +391,20 @@ defmodule NaiveDateTime do
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
## Examples
@@ -447,8 +454,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
"""
@doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -761,7 +766,7 @@ defmodule NaiveDateTime do
For readability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
### Examples
## Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
"2000-02-28 23:00:13"
@@ -908,7 +913,7 @@ defmodule NaiveDateTime do
Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise.
### Examples
## Examples
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
"2000-02-28T23:00:13"
@@ -1261,7 +1266,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1327,7 +1332,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
+14 -11
View File
@@ -225,7 +225,7 @@ defmodule Time do
@doc """
Converts the given `time` to a string.
### Examples
## Examples
iex> Time.to_string(~T[23:00:00])
"23:00:00"
@@ -334,7 +334,7 @@ defmodule Time do
format, for human readability. It also supports the "basic" format through
passing the `:basic` option.
### Examples
## Examples
iex> Time.to_iso8601(~T[23:00:13])
"23:00:13"
@@ -505,13 +505,18 @@ defmodule Time do
@doc """
Adds the `amount_to_add` of `unit`s to the given `time`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` always considers the unit to be computed according to
> the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar.
@@ -549,8 +554,6 @@ defmodule Time do
iex> result.microsecond
{21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -781,7 +784,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
@@ -837,7 +840,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
+90 -35
View File
@@ -248,6 +248,58 @@ defmodule Code do
"""
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for code formatting functions.
"""
@type format_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:line_length, pos_integer()}
| {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()}
| {:migrate, boolean()}
| {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()}
| {:migrate_unless, boolean()}
| {atom(), term()}
@typedoc """
Options for `quoted_to_algebra/2`.
"""
@type quoted_to_algebra_opt ::
{:line, pos_integer() | nil}
| {:escape, boolean()}
| {:locals_without_parens, keyword()}
| {:comments, [term()]}
@typedoc """
Options for parsing functions that convert strings to quoted expressions.
"""
@type parser_opts :: [
file: binary(),
line: pos_integer(),
column: pos_integer(),
indentation: non_neg_integer(),
columns: boolean(),
unescape: boolean(),
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (atom() -> term()),
emit_warnings: boolean()
]
@typedoc """
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
"""
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@boolean_compiler_options [
:docs,
:debug_info,
@@ -552,7 +604,7 @@ defmodule Code do
all imports, requires and aliases defined in the current environment
will be automatically carried over:
iex> require Integer
iex> require Integer, warn: false
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
iex> result
3
@@ -560,7 +612,7 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do
@@ -615,7 +667,8 @@ defmodule Code do
"""
@doc since: "1.15.0"
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
@spec with_diagnostics([log: boolean()], (-> result)) ::
{result, [diagnostic(:warning | :error)]}
when result: term()
def with_diagnostics(opts \\ [], fun) do
value = :erlang.get(:elixir_code_diagnostics)
@@ -648,7 +701,7 @@ defmodule Code do
Defaults to `true`.
"""
@doc since: "1.15.0"
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
def print_diagnostic(diagnostic, opts \\ []) do
read_snippet? = Keyword.get(opts, :snippet, true)
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@@ -672,7 +725,7 @@ defmodule Code do
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to 98. This value indicates when an expression
the document. Defaults to `98`. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
@@ -1035,9 +1088,9 @@ defmodule Code do
address the deprecation warnings.
"""
@doc since: "1.6.0"
@spec format_string!(binary, keyword) :: iodata
@spec format_string!(binary, [format_opt]) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
line_length = Keyword.get(opts, :line_length, 98)
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
to_quoted_opts =
[
@@ -1060,7 +1113,7 @@ defmodule Code do
available options.
"""
@doc since: "1.6.0"
@spec format_file!(binary, keyword) :: iodata
@spec format_file!(binary, [format_opt]) :: iodata
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
string = File.read!(file)
formatted = format_string!(string, [file: file, line: 1] ++ opts)
@@ -1098,7 +1151,7 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@@ -1129,8 +1182,15 @@ defmodule Code do
* `:line` - the line on which the script starts
* `:module` - the module to run the environment on
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """
@@ -1144,15 +1204,11 @@ defmodule Code do
## Options
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
It accepts the same options as `env_for_eval/1`.
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1171,14 +1227,14 @@ defmodule Code do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
This is useful when the code parsed is embedded within another document.
Defaults to 0.
Defaults to `0`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1263,7 +1319,7 @@ defmodule Code do
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
"""
@spec string_to_quoted(List.Chars.t(), keyword) ::
@spec string_to_quoted(List.Chars.t(), parser_opts) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
@@ -1290,7 +1346,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1341,7 +1397,7 @@ defmodule Code do
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
charlist = to_charlist(string)
@@ -1371,7 +1427,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
def string_to_quoted_with_comments!(string, opts \\ []) do
charlist = to_charlist(string)
@@ -1456,6 +1512,9 @@ defmodule Code do
## Options
This function accepts all options supported by `format_string!/2` for controlling
code formatting, plus these additional options:
* `:comments` - the list of comments associated with the quoted expression.
Defaults to `[]`. It is recommended that both `:token_metadata` and
`:literal_encoder` options are given to `string_to_quoted_with_comments/2`
@@ -1466,17 +1525,13 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`.
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
The arity may be the atom `:*`, which implies all arities of
that name. The formatter already includes a list of functions
and this option augments this list.
* `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information.
See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
"""
@doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
Inspect.Algebra.t()
def quoted_to_algebra(quoted, opts \\ []) do
quoted
|> Code.Normalizer.normalize(opts)
@@ -1686,10 +1741,10 @@ defmodule Code do
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 current module
may be compiled from scratch. `mix test` automatically disables this option via
the `:test_elixirc_options` project configuration, as there is typically no need
to infer signatures for test files.
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.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
+1
View File
@@ -158,6 +158,7 @@ defmodule Code.Formatter do
@doc """
Converts the quoted expression into an algebra document.
"""
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
def to_algebra(quoted, opts \\ []) do
comments = Keyword.get(opts, :comments, [])
+35 -8
View File
@@ -11,6 +11,26 @@ defmodule Code.Fragment do
@type position :: {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for cursor context functions.
Currently, these options are not used but reserved for future extensibility.
"""
@type cursor_opts :: []
@typedoc """
Options for converting code fragments to quoted expressions.
"""
@type container_cursor_to_quoted_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
trailing_fragment: String.t()
]
@doc ~S"""
Returns the list of lines in the given string, preserving their line endings.
@@ -172,7 +192,7 @@ defmodule Code.Fragment do
references, and more.
"""
@doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), keyword()) ::
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:block_keyword_or_binary_operator, charlist}
@@ -282,7 +302,8 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{_, _} -> {:none, 0}
{{:sigil, _}, _} -> {:none, 0}
{_, _} -> {{:operator, ~c"/"}, 1}
end
end
@@ -315,7 +336,7 @@ defmodule Code.Fragment do
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
@@ -662,7 +683,7 @@ defmodule Code.Fragment do
of examples and their return values.
"""
@doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), keyword()) ::
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
%{begin: position, end: position, context: context} | :none
when context:
{:alias, charlist}
@@ -771,6 +792,12 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, offset} ->
build_surround({:local_or_var, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:module_attribute, ~c""}, offset} ->
build_surround({:operator, ~c"@"}, reversed, line, offset)
@@ -1187,10 +1214,10 @@ defmodule Code.Fragment do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:column` - the starting column of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1209,7 +1236,7 @@ defmodule Code.Fragment do
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
@@ -1305,7 +1332,7 @@ defmodule Code.Fragment do
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp drop_tokens([], _counter), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
+1
View File
@@ -14,6 +14,7 @@ defmodule Code.Normalizer do
Wraps literals in the quoted expression to conform to the AST format expected
by the formatter.
"""
@spec normalize(Macro.t(), keyword()) :: Macro.t()
def normalize(quoted, opts \\ []) do
line = Keyword.get(opts, :line, nil)
escape = Keyword.get(opts, :escape, true)
+7 -1
View File
@@ -98,6 +98,12 @@ defmodule Config do
(assembled with `mix release`).
"""
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports}
@@ -306,7 +312,7 @@ defmodule Config do
end
@doc false
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target)
+11
View File
@@ -111,6 +111,16 @@ defmodule Config.Provider do
"""
@type config_path :: {:system, binary(), binary()} | binary()
@typedoc """
Options for `init/3`.
"""
@type init_opts :: [
extra_config: config(),
prune_runtime_sys_config_after_boot: boolean(),
reboot_system_after_config: boolean(),
validate_compile_env: [{atom(), [atom()], term()}]
]
@doc """
Invoked when initializing a config provider.
@@ -196,6 +206,7 @@ defmodule Config.Provider do
@reboot_mode_key :config_provider_reboot_mode
@doc false
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
+9 -3
View File
@@ -46,6 +46,12 @@ defmodule Config.Reader do
@behaviour Config.Provider
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@impl true
def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path)
@@ -68,7 +74,7 @@ defmodule Config.Reader do
Accepts the same options as `read!/2`.
"""
@doc since: "1.11.0"
@spec eval!(Path.t(), binary, keyword) :: keyword
@spec eval!(Path.t(), binary, config_opts) :: keyword
def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
@@ -90,7 +96,7 @@ defmodule Config.Reader do
"""
@doc since: "1.9.0"
@spec read!(Path.t(), keyword) :: keyword
@spec read!(Path.t(), config_opts) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@@ -104,7 +110,7 @@ defmodule Config.Reader do
option cannot be disabled in `read_imports!/2`.
"""
@doc since: "1.9.0"
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths"
+21 -68
View File
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
## Examples
A dynamic supervisor is started with no children and often a name:
A dynamic supervisor is started with no children and often with a name:
children = [
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
@@ -137,67 +137,6 @@ defmodule DynamicSupervisor do
A supervisor is bound to the same name registration rules as a `GenServer`.
Read more about these rules in the documentation for `GenServer`.
## Migrating from Supervisor's :simple_one_for_one
In case you were using the deprecated `:simple_one_for_one` strategy from
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
few steps.
Imagine the given "old" code:
defmodule MySupervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
Supervisor.start_child(__MODULE__, [foo, bar, baz])
end
@impl true
def init(init_arg) do
children = [
# Or the deprecated: worker(MyWorker, [init_arg])
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
]
Supervisor.init(children, strategy: :simple_one_for_one)
end
end
It can be upgraded to the DynamicSupervisor like this:
defmodule MySupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# If MyWorker is not using the new child specs, we need to pass a map:
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: [init_arg]
)
end
end
The difference is that the `DynamicSupervisor` expects the child specification
at the moment `start_child/2` is called, and no longer on the init callback.
If there are any initial arguments given on initialization, such as `[initial_arg]`,
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
"""
@behaviour GenServer
@@ -233,9 +172,10 @@ defmodule DynamicSupervisor do
@typedoc """
Return values of `start_child` functions.
Unlike `Supervisor`, this module ignores the child spec ids, so
`{:error, {:already_started, pid}}` is not returned for child specs given with the same id.
`{:error, {:already_started, pid}}` is returned however if a duplicate name is used when using
Unlike `Supervisor`, this module ignores the child spec ids,
so `{:error, {:already_started, pid}}` is not returned for child specs
given with the same id. `{:error, {:already_started, pid}}` is returned
however if a duplicate name is used when using
[name registration](`m:GenServer#module-name-registration`).
"""
@type on_start_child ::
@@ -266,6 +206,7 @@ defmodule DynamicSupervisor do
See `Supervisor` for more information about child specifications.
"""
@doc since: "1.6.1"
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
def child_spec(options) when is_list(options) do
id =
case Keyword.get(options, :name, DynamicSupervisor) do
@@ -415,6 +356,10 @@ defmodule DynamicSupervisor do
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
used when using [name registration](`m:GenServer#module-name-registration`).
This function will block the `DynamicSupervisor` until the child initializes.
When starting too many processes dynamically, you may want to use a
`PartitionSupervisor` to split the work across multiple processes.
If the child process start function returns `{:ok, child}` or `{:ok, child,
info}`, then child specification and PID are added to the supervisor and
this function returns the same value.
@@ -518,6 +463,14 @@ defmodule DynamicSupervisor do
@doc """
Terminates the given child identified by `pid`.
This function will block the `DynamicSupervisor` until the child
terminates, which may take an arbitrary amount of time if the child
is trapping exits and implements its own terminate callback.
For this reason, it is often better to ask the child process
itself to terminate, often by declaring in its child spec it has
a restart strategy of `:transient` (or `:temporary`) and then
sending it a message to stop with reason `:shutdown`.
If successful, this function returns `:ok`. If there is no process with
the given PID, this function returns `{:error, :not_found}`.
"""
@@ -528,11 +481,11 @@ defmodule DynamicSupervisor do
end
@doc """
Returns a list with information about all children.
Returns a list with information about all children of the given supervisor.
Note that calling this function when supervising a large number
of children under low memory conditions can cause an out of memory
exception.
of children under low memory conditions can bring the system down due to an
out of memory error.
This function returns a list of tuples containing:
+144 -35
View File
@@ -39,6 +39,20 @@ defprotocol Enumerable do
`reduce/3` function. All other functions exist as optimizations paths
for data structures that can implement certain properties in better
than linear time.
## Default implementation for lists
Sometimes you may want to implement this protocol for a list contained
in struct. This can be done by delegating to the `Enumerable.List` module
in the `reduce/3` implementation and providing a straight-forward
implementation for the remaining ones:
defimpl Enumerable, for: CustomStruct do
def count(struct), do: {:ok, length(struct.items)}
def member?(struct, value), do: {:ok, value in struct.items}
def slice(struct), do: {:error, __MODULE__}
def reduce(struct, acc, fun), do: Enumerable.List.reduce(struct.items, acc, fun)
end
"""
@typedoc """
@@ -766,6 +780,10 @@ defmodule Enum do
end
end
def count_until(_enumerable, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """
Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`.
@@ -787,6 +805,10 @@ defmodule Enum do
end
end
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """
Enumerates the `enumerable`, returning a list where all consecutive
duplicate elements are collapsed to a single element.
@@ -951,8 +973,8 @@ defmodule Enum do
## Examples
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
"some"
"example"
some
example
#=> :ok
"""
@@ -1214,7 +1236,7 @@ defmodule Enum do
end
@doc """
Maps the given `fun` over `enumerable` and flattens the result.
Maps the given `fun` over `enumerable` and flattens the result only one level deep.
This function returns a new enumerable built by appending the result of invoking `fun`
on each element of `enumerable` together; conceptually, this is similar to a
@@ -1262,13 +1284,16 @@ defmodule Enum do
defp flat_reverse([], acc), do: acc
@doc """
Maps and reduces an `enumerable`, flattening the given results (only one level deep).
Maps and reduces an `enumerable`, flattening the results only one level deep.
It expects an accumulator and a function that receives each enumerable
element, and must return a tuple containing a new enumerable (often a list)
with the new accumulator or a tuple with `:halt` as first element and
the accumulator as second.
Returns a 2-element tuple where the first element is the results flattened one level deep and
the second element is the last accumulator.
## Examples
iex> enumerable = 1..100
@@ -1493,6 +1518,14 @@ defmodule Enum do
to_list(enumerable)
end
def into(enumerable, collectable) when is_struct(collectable, MapSet) do
if MapSet.size(collectable) == 0 do
MapSet.new(enumerable)
else
MapSet.new(enumerable) |> MapSet.union(collectable)
end
end
def into(%_{} = enumerable, collectable) do
into_protocol(enumerable, collectable)
end
@@ -1569,8 +1602,12 @@ defmodule Enum do
map(enumerable, transform)
end
def into(%_{} = enumerable, collectable, transform) do
into_protocol(enumerable, collectable, transform)
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do
if MapSet.size(collectable) == 0 do
MapSet.new(enumerable, transform)
else
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
end
end
def into(enumerable, %_{} = collectable, transform) do
@@ -1842,7 +1879,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` according
to Erlang's term ordering.
By default, the comparison is done with the `>=` sorter function.
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
If multiple elements are considered maximal, the first one that
was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true
@@ -1909,7 +1946,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` as calculated
by the given `fun`.
By default, the comparison is done with the `>=` sorter function.
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
If multiple elements are considered maximal, the first one that
was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true
@@ -2022,7 +2059,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` according
to Erlang's term ordering.
By default, the comparison is done with the `<=` sorter function.
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
If multiple elements are considered minimal, the first one that
was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true
@@ -2089,7 +2126,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` as calculated
by the given `fun`.
By default, the comparison is done with the `<=` sorter function.
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
If multiple elements are considered minimal, the first one that
was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true
@@ -2143,28 +2180,60 @@ defmodule Enum do
@doc """
Returns a tuple with the minimal and the maximal elements in the
enumerable according to Erlang's term ordering.
enumerable.
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
Calls the provided `empty_fallback` function and returns its value if
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
By default, the comparison is done with the [`<`](`</2`) sorter function,
as the function must not return true for equal elements.
## Examples
iex> Enum.min_max([2, 3, 1])
{1, 3}
iex> Enum.min_max(["foo", "bar", "baz"])
{"bar", "foo"}
iex> Enum.min_max([], fn -> {nil, nil} end)
{nil, nil}
"""
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
when empty_result: any
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
iex> dates = [
...> ~D[2019-01-01],
...> ~D[2020-01-01],
...> ~D[2018-01-01]
...> ]
iex> Enum.min_max(dates, Date)
{~D[2018-01-01], ~D[2020-01-01]}
You can also pass a custom sorting function:
iex> Enum.min_max([2, 3, 1], &>/2)
{3, 1}
Finally, if you don't want to raise on empty enumerables, you can pass
the empty fallback:
iex> Enum.min_max([], fn -> nil end)
nil
"""
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
{element, element} | empty_result
when empty_result: any
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
def min_max(first..last//step = range, empty_fallback)
when is_function(empty_fallback, 0) do
case Range.size(range) do
0 ->
empty_fallback.()
@@ -2175,11 +2244,39 @@ defmodule Enum do
end
end
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
def min_max(enumerable, empty_fallback)
when is_function(empty_fallback, 0) do
min_max(enumerable, &</2, empty_fallback)
end
def min_max(enumerable, sorter) when is_atom(sorter) do
min_max(enumerable, min_max_sort_fun(sorter))
end
def min_max(enumerable, sorter) when is_function(sorter, 2) do
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
end
def min_max(enumerable, sorter, empty_fallback)
when is_atom(sorter) and is_function(empty_fallback, 0) do
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
end
def min_max(enumerable, sorter, empty_fallback)
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
first_fun = &[&1 | &1]
reduce_fun = fn entry, [min | max] ->
[Kernel.min(min, entry) | Kernel.max(max, entry)]
reduce_fun = fn entry, [min | max] = acc ->
cond do
sorter.(entry, min) ->
[entry | max]
sorter.(max, entry) ->
[min | entry]
true ->
acc
end
end
case reduce_by(enumerable, first_fun, reduce_fun) do
@@ -2200,8 +2297,8 @@ defmodule Enum do
Returns a tuple with the minimal and the maximal elements in the
enumerable as calculated by the given function.
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
By default, the comparison is done with the [`<`](`</2`) sorter function,
as the function must not return `true` for equal elements.
## Examples
@@ -2259,7 +2356,7 @@ defmodule Enum do
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
end
def min_max_by(enumerable, fun, sorter, empty_fallback)
@@ -2290,7 +2387,7 @@ defmodule Enum do
end
end
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
@doc """
Splits the `enumerable` in two lists according to the given function `fun`.
@@ -3611,9 +3708,14 @@ defmodule Enum do
end
def take(enumerable, amount) when is_integer(amount) and amount < 0 do
{count, fun} = slice_count_and_fun(enumerable, 1)
first = Kernel.max(amount + count, 0)
fun.(first, count - first, 1)
case slice_count_and_fun(enumerable, 1) do
{0, _fun} ->
[]
{count, fun} ->
first = Kernel.max(amount + count, 0)
fun.(first, count - first, 1)
end
end
@doc """
@@ -4089,6 +4191,11 @@ defmodule Enum do
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
[4, 6]
`zip_with/2` can be used to transpose lists of lists:
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
[[1, 3], [2, 4]]
"""
@doc since: "1.12.0"
@spec zip_with(t, ([term] -> term)) :: [term]
@@ -5006,8 +5113,7 @@ end
defimpl Enumerable, for: List do
def count(list), do: {:ok, length(list)}
def member?([], _value), do: {:ok, false}
def member?(_list, _value), do: {:error, __MODULE__}
def member?(list, value), do: {:ok, :lists.member(value, list)}
def slice([]), do: {:ok, 0, fn _, _, _ -> [] end}
def slice(_list), do: {:error, __MODULE__}
@@ -5118,6 +5224,9 @@ defimpl Enumerable, for: Range do
slice(Map.put(range, :step, step))
end
defp slice(_current, _step, 0), do: []
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
defp slice(current, _step, 1), do: [current]
defp slice(current, step, remaining) when remaining > 1 do
[current | slice(current + step, step, remaining - 1)]
end
end
+18 -36
View File
@@ -188,13 +188,12 @@ defmodule Exception do
term
|> inspect(pretty: true)
|> String.split("\n")
|> Enum.map(fn
|> Enum.map_intersperse("\n", fn
"" -> ""
line -> " " <> line
end)
|> Enum.join("\n")
message <> "\n\n" <> inspected
IO.iodata_to_binary([message, "\n\n", inspected, "\n"])
end
@doc """
@@ -288,10 +287,10 @@ defmodule Exception do
end
end
defp is_map_node?({:is_map, _, [_]}), do: true
defp is_map_node?(_), do: false
defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
defp is_map_key_node?(_), do: false
defp map_node?({:is_map, _, [_]}), do: true
defp map_node?(_), do: false
defp map_key_node?({:is_map_key, _, [_, _]}), do: true
defp map_key_node?(_), do: false
defp struct_validation_node?(
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
@@ -305,16 +304,16 @@ defmodule Exception do
defp struct_validation_node?(_), do: false
defp is_struct_macro?(
defp struct_macro?(
{:and, _,
[
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]},
%{node: node_3 = {_, _, [{_, _, [_, arg]}]}}
]}
),
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
defp is_struct_macro?(
defp struct_macro?(
{:and, _,
[
{:and, _,
@@ -329,12 +328,12 @@ defmodule Exception do
%{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}}
]}
),
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
defp is_struct_macro?(_), do: false
defp struct_macro?(_), do: false
defp translate_guard(guard) do
if is_struct_macro?(guard) do
if struct_macro?(guard) do
undo_is_struct_guard(guard)
else
guard
@@ -1389,6 +1388,7 @@ defmodule CompileError do
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
the error occurred in code that did not come from a file
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
* `:description` (`t:String.t/0`) - a description of the compile error
This is mostly raised by Elixir tooling when compiling and evaluating code.
"""
@@ -1457,20 +1457,6 @@ defmodule BadFunctionError do
end
end
defmodule BadStructError do
@moduledoc deprecated:
"This exception is deprecated alongside the struct update syntax that raises it"
defexception [:struct, :term]
@impl true
def message(exception) do
Exception.__format_message_with_term__(
"expected a struct named #{inspect(exception.struct)}, got:",
exception.term
)
end
end
defmodule BadMapError do
@moduledoc """
An exception raised when a map is expected, but something else was given.
@@ -1912,7 +1898,7 @@ defmodule UndefinedFunctionError do
end
defp format_fa({_dist, fun, arity}) do
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp exports_for(module) do
@@ -1944,8 +1930,8 @@ defmodule FunctionClauseError do
For example:
iex> URI.parse(:wrong_argument)
** (FunctionClauseError) no function clause matching in URI.parse/1
iex> List.duplicate(:ok, -3)
** (FunctionClauseError) no function clause matching in List.duplicate/2
The following fields of this exception are public and can be accessed freely:
@@ -2244,7 +2230,7 @@ defmodule KeyError do
case suggestions do
[] -> []
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
suggestions -> ["\nDid you mean:\n\n" | format_suggestions(suggestions)]
end
end
@@ -2253,7 +2239,7 @@ defmodule KeyError do
|> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|> Enum.take(@max_suggestions)
|> Enum.sort(&(elem(&1, 1) <= elem(&2, 1)))
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
end
end
@@ -2605,10 +2591,6 @@ defmodule ErlangError do
%BadFunctionError{term: term}
end
def normalize({:badstruct, struct, term}, _stacktrace) do
%BadStructError{struct: struct, term: term}
end
def normalize({:badmatch, term}, _stacktrace) do
%MatchError{term: term}
end
+21 -8
View File
@@ -317,7 +317,7 @@ defmodule File do
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
* `:eperm` - missed required permisions
* `:eperm` - missed required permissions
## Examples
@@ -694,7 +694,7 @@ defmodule File do
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
{:error, :enoent}
File.touch("/tmp/a.txt", 1544519753)
File.touch("/tmp/a.txt", 1_544_519_753)
#=> :ok
"""
@@ -706,7 +706,7 @@ defmodule File do
path = IO.chardata_to_string(path)
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
:ok <- write(path, "", [:append]),
:ok <- write(path, "", [:raw, :append]),
do: :elixir_utils.change_universal_time(path, time)
end
@@ -714,7 +714,7 @@ defmodule File do
path = IO.chardata_to_string(path)
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
:ok <- write(path, "", [:append]),
:ok <- write(path, "", [:raw, :append]),
do: :elixir_utils.change_posix_time(path, time)
end
@@ -733,7 +733,7 @@ defmodule File do
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
File.touch!("/tmp/a.txt", 1544519753)
File.touch!("/tmp/a.txt", 1_544_519_753)
"""
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
@@ -1114,6 +1114,8 @@ defmodule File do
explicitly disallow this behavior. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned.
Special files such as device files, sockets, and named pipes are not copied.
## Options
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
@@ -1224,7 +1226,13 @@ defmodule File do
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
do_cp_file(src, dest, on_conflict, acc)
case do_cp_file(src, dest, on_conflict, acc) do
# we don't have a way to make a distinction between a non-existing src
# or dest being a non-existing dir in the case of :enoent,
# but we already know that src exists here.
{:error, :enoent, _} -> {:error, :enoent, dest}
other -> other
end
{:ok, :symlink} ->
case :file.read_link(src) do
@@ -1256,7 +1264,7 @@ defmodule File do
end
{:ok, _} ->
{:error, :eio, src}
acc
{:error, reason} ->
{:error, reason, src}
@@ -2159,7 +2167,12 @@ defmodule File do
def stream!(path, line_or_bytes, modes)
def stream!(path, modes, line_or_bytes) when is_list(modes) do
# TODO: Deprecate this on Elixir v1.20
# TODO: Remove me on Elixir 2.0
IO.warn(
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
"invoke File.stream!(path, line_or_bytes, modes) instead"
)
stream!(path, line_or_bytes, modes)
end
+1 -1
View File
@@ -42,7 +42,7 @@ defmodule Float do
To learn more about floating-point arithmetic visit:
* [0.30000000000000004.com](http://0.30000000000000004.com/)
* [0.30000000000000004.com](https://0.30000000000000004.com/)
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
"""
+36 -1
View File
@@ -349,6 +349,41 @@ defmodule GenServer do
message arriving, `handle_info/2` is called with `:timeout` as the first
argument.
For example:
defmodule Counter do
use GenServer
@timeout to_timeout(second: 5)
@impl true
def init(count) do
{:ok, count, @timeout}
end
@impl true
def handle_call(:increment, _from, count) do
new_count = count + 1
{:reply, new_count, new_count, @timeout}
end
@impl true
def handle_info(:timeout, count) do
{:stop, :normal, count}
end
end
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
after the initialization or after the last `:increment` call:
{:ok, counter_pid} = GenServer.start(Counter, 50)
GenServer.call(counter_pid, :increment)
#=> 51
# After 5 seconds
Process.alive?(counter_pid)
#=> false
## When (not) to use a GenServer
So far, we have learned that a `GenServer` can be used as a supervised process
@@ -488,7 +523,7 @@ defmodule GenServer do
* [GenServer - Elixir's Getting Started Guide](genservers.md)
* [`:gen_server` module documentation](`:gen_server`)
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers)
"""
+6 -29
View File
@@ -275,8 +275,6 @@ defprotocol Inspect do
end
defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
@@ -569,6 +567,7 @@ defimpl Inspect, for: Regex do
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
@@ -662,36 +661,14 @@ end
defimpl Inspect, for: Any do
def inspect(%module{} = struct, opts) do
try do
module.__info__(:struct)
rescue
_ -> Inspect.Map.inspect_as_map(struct, opts)
else
info ->
if valid_struct?(info, struct) do
info =
for %{field: field} = map <- info,
field != :__exception__,
do: map
info =
for %{field: field} = map <- module.__info__(:struct),
field != :__exception__,
do: map
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
else
Inspect.Map.inspect_as_map(struct, opts)
end
end
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
end
defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1)
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
do: valid_struct?(info, struct, count - 1)
defp valid_struct?([], _struct, 0),
do: true
defp valid_struct?(_fields, _struct, _count),
do: false
def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
+57 -5
View File
@@ -46,7 +46,7 @@ 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
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.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
@@ -115,11 +115,28 @@ defmodule Inspect.Opts do
width: non_neg_integer | :infinity
}
@typedoc """
Options for building an `Inspect.Opts` struct with `new/1`.
"""
@type new_opt ::
{:base, :decimal | :binary | :hex | :octal}
| {:binaries, :infer | :as_binaries | :as_strings}
| {:charlists, :infer | :as_lists | :as_charlists}
| {:custom_options, keyword}
| {:inspect_fun, (any, t -> Inspect.Algebra.t())}
| {:limit, non_neg_integer | :infinity}
| {:pretty, boolean}
| {:printable_limit, non_neg_integer | :infinity}
| {:safe, boolean}
| {:structs, boolean}
| {:syntax_colors, [{color_key, IO.ANSI.ansidata()}]}
| {:width, non_neg_integer | :infinity}
@doc """
Builds an `Inspect.Opts` struct.
"""
@doc since: "1.13.0"
@spec new(keyword()) :: t
@spec new([new_opt()]) :: t
def new(opts) do
struct(%Inspect.Opts{inspect_fun: default_inspect_fun()}, opts)
end
@@ -324,6 +341,14 @@ defmodule Inspect.Algebra do
quote do: {:doc_color, unquote(doc), unquote(color)}
end
@typedoc """
Options for container documents.
"""
@type container_opts :: [
separator: String.t(),
break: :strict | :flex | :maybe
]
@docs [
:doc_break,
:doc_collapse,
@@ -371,7 +396,7 @@ defmodule Inspect.Algebra do
def to_doc_with_opts(term, opts)
def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do
if opts.structs do
if opts.structs and valid_struct?(struct) do
try do
fun.(struct, opts)
rescue
@@ -428,6 +453,26 @@ defmodule Inspect.Algebra do
fun.(arg, opts) |> pack_opts(opts)
end
defp valid_struct?(%module{} = struct) do
try do
module.__info__(:struct)
rescue
_ -> false
else
info ->
valid_struct?(info, struct, map_size(struct) - 1)
end
end
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
do: valid_struct?(info, struct, count - 1)
defp valid_struct?([], _struct, 0),
do: true
defp valid_struct?(_fields, _struct, _count),
do: false
defp pack_opts({_doc, %Inspect.Opts{}} = doc_opts, _opts), do: doc_opts
defp pack_opts(doc, opts), do: {doc, opts}
@@ -440,7 +485,14 @@ defmodule Inspect.Algebra do
updated options from inspection.
"""
@doc since: "1.6.0"
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(
t,
[term],
t,
Inspect.Opts.t(),
(term, Inspect.Opts.t() -> t),
container_opts()
) ::
t
def container_doc(left, collection, right, inspect_opts, fun, opts \\ []) do
container_doc_with_opts(left, collection, right, inspect_opts, fun, opts) |> elem(0)
@@ -496,7 +548,7 @@ defmodule Inspect.Algebra do
t,
Inspect.Opts.t(),
(term, Inspect.Opts.t() -> t),
keyword()
container_opts()
) ::
{t, Inspect.Opts.t()}
def container_doc_with_opts(left, collection, right, inspect_opts, fun, opts \\ [])
+29
View File
@@ -172,6 +172,35 @@ defmodule Integer do
end
end
@doc """
Performs a ceiled integer division.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
This function performs a *ceiled* integer division, which means that
the result will always be rounded towards positive infinity.
## Examples
iex> Integer.ceil_div(5, 2)
3
iex> Integer.ceil_div(6, -4)
-1
iex> Integer.ceil_div(-99, 2)
-49
"""
@doc since: "1.20.0"
@spec ceil_div(integer, neg_integer | pos_integer) :: integer
def ceil_div(dividend, divisor) do
if not :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
div(dividend, divisor) + 1
else
div(dividend, divisor)
end
end
@doc """
Returns the ordered digits for the given `integer`.
+61 -14
View File
@@ -128,6 +128,22 @@ defmodule IO do
@type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
@typedoc """
Stacktrace information as keyword options for `warn/2`.
At least `:file` is required. Other options are optional and used
to provide more precise location information.
"""
@type warn_stacktrace_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
module: module(),
function: {atom(), arity()}
]
defguardp is_device(term) when is_atom(term) or is_pid(term)
defguardp is_iodata(data) when is_list(data) or is_binary(data)
@@ -346,7 +362,10 @@ defmodule IO do
#=> my_app.ex:4: MyApp.main/1
"""
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
@spec warn(
chardata | String.Chars.t(),
Exception.stacktrace() | warn_stacktrace_opts() | Macro.Env.t()
) ::
:ok
def warn(message, stacktrace_info)
@@ -448,13 +467,15 @@ defmodule IO do
## Examples
The following code:
IO.inspect(<<0, 1, 2>>, width: 40)
Prints:
<<0, 1, 2>>
We can use the `:label` option to decorate the output:
You can use the `:label` option to decorate the output:
IO.inspect(1..100, label: "a wonderful range")
@@ -462,21 +483,23 @@ defmodule IO do
a wonderful range: 1..100
The `:label` option is especially useful with pipelines:
Inspect truncates large inputs by default. The `:printable_limit` controls
the limit for strings and other string-like constructs (such as charlists):
[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")
|> Enum.sum()
"abc"
|> String.duplicate(9001)
|> IO.inspect(printable_limit: :infinity)
Prints:
For containers such as lists, maps, and tuples, the number of entries
is managed by the `:limit` option:
before: [1, 2, 3]
after: [2, 4, 6]
1..100
|> Enum.map(& {&1, &1})
|> Enum.into(%{})
|> IO.inspect(limit: :infinity)
"""
@spec inspect(item, keyword) :: item when item: var
@spec inspect(item, inspect_opts) :: item when item: var
def inspect(item, opts \\ []) do
inspect(:stdio, item, opts)
end
@@ -486,9 +509,10 @@ defmodule IO do
See `inspect/2` for a full list of options.
"""
@spec inspect(device, item, keyword) :: item when item: var
@spec inspect(device, item, inspect_opts) :: item when item: var
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
{label, opts} = Keyword.pop(opts, :label)
label = if label, do: [to_chardata(label), ": "], else: []
opts = Inspect.Opts.new(opts)
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
chardata = Inspect.Algebra.format(doc, opts.width)
@@ -772,6 +796,29 @@ defmodule IO do
:erlang.iolist_size(iodata)
end
@doc """
Checks if an IO data (the length is zero).
For more information about IO data, see the ["IO data"](#module-io-data)
section in the module documentation.
## Examples
iex> IO.iodata_empty?([])
true
iex> IO.iodata_empty?([""])
true
iex> IO.iodata_empty?([1, 2 | <<3, 4>>])
false
"""
@doc since: "1.20.0"
@spec iodata_empty?(iodata) :: boolean
def iodata_empty?(""), do: true
def iodata_empty?([]), do: true
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
def iodata_empty?(_), do: false
@doc false
def each_stream(device, line_or_codepoints) do
case read(device, line_or_codepoints) do
+18 -4
View File
@@ -5,6 +5,20 @@
defmodule IO.ANSI.Docs do
@moduledoc false
@type print_opts :: [
enabled: boolean(),
doc_bold: [IO.ANSI.ansicode()],
doc_code: [IO.ANSI.ansicode()],
doc_headings: [IO.ANSI.ansicode()],
doc_metadata: [IO.ANSI.ansicode()],
doc_quote: [IO.ANSI.ansicode()],
doc_inline_code: [IO.ANSI.ansicode()],
doc_table_heading: [IO.ANSI.ansicode()],
doc_title: [IO.ANSI.ansicode()],
doc_underline: [IO.ANSI.ansicode()],
width: pos_integer()
]
@bullet_text_unicode "• "
@bullet_text_ascii "* "
@bullets [?*, ?-, ?+]
@@ -30,7 +44,7 @@ defmodule IO.ANSI.Docs do
Values for the color settings are strings with
comma-separated ANSI values.
"""
@spec default_options() :: keyword
@spec default_options() :: print_opts
def default_options do
[
enabled: true,
@@ -52,7 +66,7 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_headings([String.t()], keyword) :: :ok
@spec print_headings([String.t()], print_opts) :: :ok
def print_headings(headings, options \\ []) do
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
@@ -77,7 +91,7 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_metadata(map, keyword) :: :ok
@spec print_metadata(map, print_opts) :: :ok
def print_metadata(metadata, options \\ []) when is_map(metadata) do
options = Keyword.merge(default_options(), options)
print_each_metadata(metadata, options) && IO.write("\n")
@@ -115,7 +129,7 @@ defmodule IO.ANSI.Docs do
It takes a set of `options` defined in `default_options/0`.
"""
@spec print(term(), String.t(), keyword) :: :ok
@spec print(term(), String.t(), print_opts) :: :ok
def print(doc, format, options \\ [])
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
+29 -8
View File
@@ -8,24 +8,29 @@ defprotocol JSON.Encoder do
If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
@derive {JSON.Encoder, only: [...]}
defstruct ...
It is also possible to encode all fields or skip some fields via the
`:except` option:
Additionally, you can exclude specific fields using the `:except` option or
encode all fields by omitting both options entirely, but these should be used
with caution:
@derive {JSON.Encoder, except: [...]}
defstruct ...
@derive JSON.Encoder
defstruct ...
> #### Leaking Private Information {: .error}
>
> The `:except` approach should be used carefully to avoid
> accidentally leaking private information when new fields are added.
> Prefer using `:only` to avoid accidentally leaking private information when
> new fields are added. Other approaches should be used with auction.
Finally, if you don't own the struct you want to encode to JSON,
you may use `Protocol.derive/3` placed outside of any module:
You can also use `Protocol.derive/3` if you don't own the struct that you want
to encode to JSON:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)
"""
@@ -328,6 +333,22 @@ defmodule JSON do
| {:invalid_byte, non_neg_integer(), byte()}
| {:unexpected_sequence, non_neg_integer(), binary()}
@typedoc """
Decoders for customizing JSON decoding behavior.
"""
@type decoders :: [
array_start: (term() -> term()),
array_push: (term(), term() -> term()),
array_finish: (term(), term() -> {term(), term()}),
object_start: (term() -> term()),
object_push: (term(), term(), term() -> term()),
object_finish: (term(), term() -> {term(), term()}),
float: (String.t() -> term()),
integer: (String.t() -> term()),
string: (String.t() -> term()),
null: term()
]
@doc ~S"""
Decodes the given JSON.
@@ -381,7 +402,7 @@ defmodule JSON do
For streaming decoding, see Erlang's [`:json`](`:json`) module.
"""
@spec decode(binary(), term(), keyword()) ::
@spec decode(binary(), term(), decoders()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
+50 -49
View File
@@ -231,7 +231,7 @@ defmodule Kernel do
Finally, note there is an overall structural sorting order, called
"Term Ordering", defined below. This order is provided for reference
purposes, it is not required by Elixir developers to know it by heart.
purposes, it is not required for Elixir developers to know it by heart.
### Term ordering
@@ -1999,6 +1999,12 @@ defmodule Kernel do
{:case, extra ++ meta, args}
end
defp x_is_false_or_nil do
quote generated: true do
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
end
end
@doc """
Strictly boolean "or" operator.
@@ -2101,7 +2107,7 @@ defmodule Kernel do
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
x when unquote(x_is_false_or_nil()) -> false
_ -> true
end
end
@@ -2115,7 +2121,7 @@ defmodule Kernel do
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
x when unquote(x_is_false_or_nil()) -> true
_ -> false
end
end
@@ -2456,7 +2462,7 @@ defmodule Kernel do
See the "Deriving" section of the documentation of the `Inspect`
protocol for more information.
"""
@spec inspect(Inspect.t(), keyword) :: String.t()
@spec inspect(Inspect.t(), [Inspect.Opts.new_opt()]) :: String.t()
def inspect(term, opts \\ []) when is_list(opts) do
opts = Inspect.Opts.new(opts)
@@ -2815,7 +2821,7 @@ defmodule Kernel do
This is most commonly used in pipelines, using the `|>/2` operator, allowing you
to pipe a value to a function outside of its first argument.
### Examples
## Examples
iex> 1 |> then(fn x -> x * 2 end)
2
@@ -3524,8 +3530,7 @@ defmodule Kernel do
end
@doc """
A convenience macro that checks if the right side (an expression) matches the
left side (a pattern).
A convenience macro that checks if the result of `expression` matches `pattern`.
## Examples
@@ -3603,7 +3608,7 @@ defmodule Kernel do
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
"""
defmacro match?(pattern, expr) do
defmacro match?(pattern, expression) do
success =
quote do
unquote(pattern) -> true
@@ -3614,7 +3619,7 @@ defmodule Kernel do
_ -> false
end
{:case, [], [expr, [do: success ++ failure]]}
{:case, [], [expression, [do: success ++ failure]]}
end
@doc """
@@ -3814,19 +3819,6 @@ defmodule Kernel do
{_, doc} when doc_attr? ->
do_at_escape(name, doc)
%{__struct__: Regex, source: source, opts: opts} = regex ->
# TODO: Remove this in Elixir v2.0
IO.warn(
"storing and reading regexes from module attributes is deprecated, " <>
"inline the regex inside the function definition instead",
env
)
case :erlang.system_info(:otp_release) < [?2, ?8] do
true -> do_at_escape(name, regex)
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
end
value ->
do_at_escape(name, value)
end
@@ -3872,7 +3864,9 @@ defmodule Kernel do
defp do_at_escape(name, value) do
try do
:elixir_quote.escape(value, :none, false)
# mark module attrs as shallow-generated since the ast for their representation
# might contain opaque terms
Macro.escape(value, generated: true)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
@@ -4058,7 +4052,7 @@ defmodule Kernel do
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(condition) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
_ -> unquote(do_clause)
end
end
@@ -4379,7 +4373,7 @@ defmodule Kernel do
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x when unquote(x_is_false_or_nil()) ->
x
_ ->
@@ -4422,7 +4416,7 @@ defmodule Kernel do
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x when unquote(x_is_false_or_nil()) ->
unquote(right)
x ->
@@ -4770,8 +4764,8 @@ defmodule Kernel do
defp in_range(left, first, last, step) do
quoted =
quote do
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
:erlang.is_integer(unquote(last)) and
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
unquote(generated_is_integer(last)) and
((:erlang.>(unquote(step), 0) and
unquote(increasing_compare(left, first, last))) or
(:erlang.<(unquote(step), 0) and
@@ -4787,9 +4781,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step > 0 do
quoted =
quote do
:erlang.andalso(
:erlang.is_integer(unquote(left)),
quote generated: true do
Kernel.and(
unquote(generated_is_integer(left)),
unquote(increasing_compare(left, first, last))
)
end
@@ -4799,9 +4793,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step < 0 do
quoted =
quote do
:erlang.andalso(
:erlang.is_integer(unquote(left)),
quote generated: true do
Kernel.and(
unquote(generated_is_integer(left)),
unquote(decreasing_compare(left, first, last))
)
end
@@ -4815,7 +4809,7 @@ defmodule Kernel do
defp in_range_step(quoted, left, first, step) do
quote do
:erlang.andalso(
Kernel.and(
unquote(quoted),
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
)
@@ -4824,7 +4818,7 @@ defmodule Kernel do
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
:lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
:lists.foldl(&quote(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
end
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
@@ -4834,7 +4828,7 @@ defmodule Kernel do
[tail_head | tail] ->
quote do
:erlang.orelse(
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
)
@@ -4842,7 +4836,7 @@ defmodule Kernel do
tail when in_body? ->
quote do
:erlang.orelse(
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
:lists.member(unquote(left), unquote(tail))
)
@@ -4857,9 +4851,13 @@ defmodule Kernel do
quote(do: :erlang."=:="(unquote(left), unquote(right)))
end
defp generated_is_integer(arg) do
quote generated: true, do: :erlang.is_integer(unquote(arg))
end
defp increasing_compare(var, first, last) do
quote do
:erlang.andalso(
Kernel.and(
:erlang.>=(unquote(var), unquote(first)),
:erlang."=<"(unquote(var), unquote(last))
)
@@ -4868,7 +4866,7 @@ defmodule Kernel do
defp decreasing_compare(var, first, last) do
quote do
:erlang.andalso(
Kernel.and(
:erlang."=<"(unquote(var), unquote(first)),
:erlang.>=(unquote(var), unquote(last))
)
@@ -5197,7 +5195,7 @@ defmodule Kernel do
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
%{} ->
:elixir_quote.escape(block, :none, false)
:elixir_quote.escape(block, :escape, false)
end
versioned_vars = env.versioned_vars
@@ -5477,7 +5475,7 @@ defmodule Kernel do
store =
case unquoted_expr or unquoted_call do
true ->
:elixir_quote.escape({call, expr}, :none, true)
:elixir_quote.escape({call, expr}, :escape, true)
false ->
key = :erlang.unique_integer()
@@ -5605,8 +5603,8 @@ defmodule Kernel do
## Types
It is recommended to define types for structs. By convention, such a type
is called `t`. To define a struct inside a type, the struct literal syntax
is used:
is called `t`. To define a type for a struct, the struct literal syntax is
used:
defmodule User do
defstruct name: "John", age: 25
@@ -6389,7 +6387,7 @@ defmodule Kernel do
With a timeout:
iex> to_timeout(5400000)
iex> to_timeout(5_400_000)
5400000
iex> to_timeout(:infinity)
:infinity
@@ -6630,11 +6628,13 @@ defmodule Kernel do
defmacro sigil_r(term, modifiers)
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1)
compile_regex(binary, options)
end
defmacro sigil_r({:<<>>, meta, pieces}, options) do
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
compile_regex(tuple, options)
end
@@ -6653,13 +6653,14 @@ defmodule Kernel do
end
defp compile_regex(binary_or_tuple, options) do
# TODO: Remove this when we require Erlang/OTP 28+
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
bin_opts = :binary.list_to_bin(options)
case is_binary(binary_or_tuple) do
true ->
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
Macro.escape(Regex.compile!(binary_or_tuple, bin_opts))
false ->
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts)))
end
end
+36
View File
@@ -21,6 +21,15 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout)
end
@doc """
Invoked during module expansion to annotate a require
must be warned if unused.
"""
def warn_require(pid, meta, module, alias) do
:gen_server.cast(pid, {:warn_require, module, meta, alias})
module
end
@doc """
Invoked during module expansion to annotate an alias
must be warned if unused.
@@ -57,6 +66,11 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_export, module})
end
@doc false
def add_require(pid, module, meta) when is_atom(module) do
:gen_server.cast(pid, {:add_require, module, meta})
end
@doc false
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
@@ -119,12 +133,18 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :unused_aliases, @timeout)
end
@doc false
def collect_unused_requires(pid) do
:gen_server.call(pid, :unused_requires, @timeout)
end
# Callbacks
def init(:ok) do
state = %{
aliases: %{},
imports: %{},
requires: %{},
references: %{},
exports: %{},
cache: %{},
@@ -150,6 +170,18 @@ defmodule Kernel.LexicalTracker do
{:reply, Enum.sort(imports), state}
end
def handle_call(:unused_requires, _from, state) do
%{references: references, aliases: aliases} = state
unused_requires =
for {module, {meta, alias}} <- state.requires,
Map.get(references, module) != :compile do
{module, meta, alias, Map.get(aliases, alias) == :used}
end
{:reply, Enum.sort(unused_requires), state}
end
def handle_call(:references, _from, state) do
{compile, runtime} = partition(Map.to_list(state.references), [], [])
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
@@ -245,6 +277,10 @@ defmodule Kernel.LexicalTracker do
{:noreply, put_in(state.imports[module][@warn_key], true)}
end
def handle_cast({:warn_require, module, meta, alias}, state) do
{:noreply, put_in(state.requires[module], {meta, alias})}
end
@doc false
def handle_info(_msg, state) do
{:noreply, state}
+113 -29
View File
@@ -16,11 +16,43 @@ defmodule Kernel.ParallelCompiler do
@type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
@type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
@typedoc """
Options for parallel compilation functions.
"""
@type compile_opts :: [
after_compile: (-> term()),
each_file: (Path.t() -> term()),
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
{:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
profile: :time,
dest: Path.t(),
beam_timestamp: term(),
return_diagnostics: boolean(),
max_concurrency: pos_integer()
]
@typedoc """
Options for requiring files in parallel.
"""
@type require_opts :: [
each_file: (Path.t() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
max_concurrency: pos_integer(),
return_diagnostics: boolean()
]
@doc """
Starts a task for parallel compilation.
"""
# TODO: Deprecate this on Elixir v1.20.
@doc deprecated: "Use `pmap/2` instead"
# TODO: Remove me on Elixir 2.0
@deprecated "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun)
send(task.pid, ref)
@@ -114,10 +146,9 @@ defmodule Kernel.ParallelCompiler do
the current file stops being compiled until the dependency is
resolved.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -177,15 +208,16 @@ defmodule Kernel.ParallelCompiler do
* `:beam_timestamp` - the modification timestamp to give all BEAM files
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
Defaults to the number of schedulers online, or at least `2`.
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
@spec compile([Path.t()], compile_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile(files, options \\ []) when is_list(options) do
@@ -198,7 +230,7 @@ defmodule Kernel.ParallelCompiler do
See `compile/2` for more information.
"""
@doc since: "1.6.0"
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
@spec compile_to_path([Path.t()], Path.t(), compile_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
@@ -211,10 +243,9 @@ defmodule Kernel.ParallelCompiler do
Opposite to compile, dependencies are not attempted to be
automatically solved between files.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -231,11 +262,15 @@ defmodule Kernel.ParallelCompiler do
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
Defaults to the number of schedulers online, or at least `2`.
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
"""
@doc since: "1.6.0"
@spec require([Path.t()], keyword()) ::
@spec require([Path.t()], require_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def require(files, options \\ []) when is_list(options) do
@@ -286,7 +321,10 @@ defmodule Kernel.ParallelCompiler do
if Keyword.get(options, :return_diagnostics, false) do
{status, modules_or_errors, info}
else
IO.warn("you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler")
IO.warn(
"you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions"
)
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
modules_or_errors =
@@ -342,27 +380,73 @@ defmodule Kernel.ParallelCompiler do
end
end
defp write_module_binaries(result, {:compile, path}, timestamp) do
File.mkdir_p!(path)
Code.prepend_path(path)
defp write_module_binaries(result, {:compile, path}, state) when map_size(result) > 0 do
profile(state, "writing modules to disk", fn ->
File.mkdir_p!(path)
Code.prepend_path(path)
timestamp = state.beam_timestamp
for {{:module, module}, {binary, _}} when is_binary(binary) <- result do
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
File.write!(full_path, binary)
if timestamp, do: File.touch!(full_path, timestamp)
module
end
# We fan-out the writes as that improves performance
# when writing hundreds of beam files. This is cheap as
# we only transfer atoms and binaries across processes.
pool_size = min(map_size(result), state.schedulers)
pool_list =
for _ <- 1..pool_size do
spawn_link(fn -> write_loop(path, timestamp) end)
end
pool_tuple = List.to_tuple(pool_list)
{modules, _} =
Enum.flat_map_reduce(result, 0, fn
{{:module, module}, {binary, _}}, scheduler when is_binary(binary) ->
send(elem(pool_tuple, scheduler), {:write, module, binary})
{[module], rem(scheduler + 1, pool_size)}
_, scheduler ->
{[], scheduler}
end)
pool_refs =
for pid <- pool_list do
ref = Process.monitor(pid)
send(pid, :done)
ref
end
for ref <- pool_refs do
receive do
{:DOWN, ^ref, _, _, _} -> :ok
end
end
modules
end)
end
defp write_module_binaries(result, _output, _timestamp) do
defp write_module_binaries(result, _output, _state) do
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
end
defp write_loop(path, timestamp) do
receive do
{:write, module, binary} ->
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
File.write!(full_path, binary, [:raw])
if timestamp, do: File.touch!(full_path, timestamp)
write_loop(path, timestamp)
:done ->
:ok
end
end
## Verification
defp verify_modules(result, compile_warnings, dependent_modules, state) do
modules = write_module_binaries(result, state.output, state.beam_timestamp)
_ = state.after_compile.()
modules = write_module_binaries(result, state.output, state)
profile(state, "after compile callback", state.after_compile)
runtime_warnings =
if state.verification? do
+22 -34
View File
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
Let's give it a try on IEx:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
iex> opts = %{"width" => 10, "height" => 15}
iex> with {:ok, width} <- Map.fetch(opts, "width"),
...> {:ok, height} <- Map.fetch(opts, "height") do
...> {:ok, width * height}
...> end
{:ok, 150}
@@ -1603,21 +1603,13 @@ defmodule Kernel.SpecialForms do
If all clauses match, the `do` block is executed, returning its result.
Otherwise the chain is aborted and the non-matched value is returned:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
iex> opts = %{"width" => 10}
iex> with {:ok, width} <- Map.fetch(opts, "width"),
...> {:ok, height} <- Map.fetch(opts, "height") do
...> {:ok, width * height}
...> end
:error
Guards can be used in patterns as well:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"}
As in `for/1`, variables bound inside `with/1` won't be accessible
outside of `with/1`.
@@ -1661,22 +1653,18 @@ defmodule Kernel.SpecialForms do
An `else` option can be given to modify what is being returned from
`with` in the case of a failed match:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
with {:ok, content} <- File.read(path),
:ok <- File.write(path, [content, "!"]) do
:ok
else
{:error, reason} ->
Logger.error("could not append ! to \#{path} with reason: \#{reason}")
:error
end
The `else` block works like a `case`: it can have multiple clauses,
and the first match will be used. Variables bound inside `with` (such as
`width` in this example) are not available in the `else` block.
and the first match will be used. Variables bound inside `with`
(such as `content` in this example) are not available in the `else` block.
If an `else` block is used and there are no matching clauses, a `WithClauseError`
exception is raised.
@@ -1987,13 +1975,13 @@ defmodule Kernel.SpecialForms do
While it is not possible to match against multiple patterns in a single
clause, it's possible to match against multiple values by using guards:
iex> case :two do
...> value when value in [:one, :two] ->
iex> case 2 do
...> value when value in [1, 2] ->
...> "#{value} has been matched"
...> :three ->
...> "three has been matched"
...> 3 ->
...> "3 has been matched"
...> end
"two has been matched"
"2 has been matched"
"""
defmacro case(condition, clauses), do: error!([condition, clauses])
@@ -2355,7 +2343,7 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args])
@doc """
Checks if there is a message matching any of the given clauses in the current
Consumes the first message matching any of the given clauses in the current
process mailbox.
If there is no matching message, the current process waits until a matching
+9 -1
View File
@@ -877,7 +877,15 @@ defmodule Kernel.Typespec do
defp typespec({:fun, meta, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :fun, args}, state}
if args != [] do
IO.warn(
"fun/#{length(args)} is not valid in typespecs. Either specify fun() or use (... -> return) instead",
caller
)
end
{{:type, location(meta), :fun, []}, state}
end
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
+5 -4
View File
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :none, false)
:elixir_quote.escape(val, {:struct, module}, false)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
raise ArgumentError,
"invalid default value for struct field #{key}, " <> Exception.message(e)
else
_ -> {key, val}
end
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body =
case bootstrapped? do
@@ -217,7 +218,7 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
mapper = fn {key, val} ->
%{field: key, default: val}
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
+9 -8
View File
@@ -437,7 +437,7 @@ defmodule Keyword do
end
@doc """
Gets the value from `key` and updates it, all in one pass.
Gets the value for `key` and updates it in one pass, deleting duplicate keys.
The `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the current value
@@ -483,7 +483,7 @@ defmodule Keyword do
defp get_and_update([{key, current} | t], acc, key, fun) do
case fun.(current) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | t])}
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop ->
{current, :lists.reverse(acc, t)}
@@ -509,7 +509,8 @@ defmodule Keyword do
end
@doc """
Gets the value under `key` and updates it. Raises if there is no `key`.
Gets the value for `key` and updates it in one pass, deleting duplicate keys,
raising if `key` can't be found in `keywords`.
The `fun` argument receives the value under `key` and must return a
two-element tuple: the current value (the retrieved value, which can be
@@ -545,21 +546,21 @@ defmodule Keyword do
get_and_update!(keywords, key, fun, [])
end
defp get_and_update!([{key, value} | keywords], key, fun, acc) do
defp get_and_update!([{key, value} | t], key, fun, acc) do
case fun.(value) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop ->
{value, :lists.reverse(acc, keywords)}
{value, :lists.reverse(acc, t)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
get_and_update!(keywords, key, fun, [e | acc])
defp get_and_update!([{_, _} = h | t], key, fun, acc) do
get_and_update!(t, key, fun, [h | acc])
end
defp get_and_update!([], key, _fun, acc) when is_atom(key) do
+6 -5
View File
@@ -187,9 +187,10 @@ defmodule List do
"""
@spec duplicate(any, 0) :: []
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
def duplicate(elem, n) do
:lists.duplicate(n, elem)
end
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
defp duplicate(0, _elem, acc), do: acc
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
@doc """
Flattens the given `list` of nested lists.
@@ -915,7 +916,7 @@ defmodule List do
If `prefix` is an empty list, it returns `true`.
### Examples
## Examples
iex> List.starts_with?([1, 2, 3], [1, 2])
true
@@ -945,7 +946,7 @@ defmodule List do
If `suffix` is an empty list, it returns `true`.
### Examples
## Examples
iex> List.ends_with?([1, 2, 3], [2, 3])
true
-6
View File
@@ -19,12 +19,6 @@ defprotocol List.Chars do
"""
@spec to_charlist(t) :: charlist
def to_charlist(term)
@doc false
@deprecated "Use List.Chars.to_charlist/1 instead"
Kernel.def to_char_list(term) do
__MODULE__.to_charlist(term)
end
end
defimpl List.Chars, for: Atom do
+156 -52
View File
@@ -197,6 +197,16 @@ defmodule Macro do
@typedoc "A captured remote function in the format of &Mod.fun/arity"
@type captured_remote_function :: fun
@type escape_opts :: [
unquote: boolean(),
prune_metadata: boolean(),
generated: boolean()
]
@type inspect_atom_opts :: [
escape: (binary(), char() -> binary())
]
@doc """
Breaks a pipeline expression into a list.
@@ -498,13 +508,11 @@ defmodule Macro do
Generates AST nodes for a given number of required argument
variables using `Macro.unique_var/2`.
The second argument is generally the macro caller's module.
## Examples
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
iex> {:arg1, [counter: c1], __MODULE__} = var1
iex> {:arg2, [counter: c2], __MODULE__} = var2
iex> is_integer(c1) and is_integer(c2)
true
[var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)
"""
@doc since: "1.11.3"
@@ -560,11 +568,11 @@ defmodule Macro do
generate another variable, with its own unique counter.
See `var/2` for an alternative.
The second argument is generally the macro caller's module.
## Examples
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
iex> is_integer(c)
true
var = Macro.unique_var(:foo, __CALLER__.module)
"""
@doc since: "1.11.3"
@@ -793,12 +801,18 @@ defmodule Macro do
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions unescaped, effectively unquoting
the contents on escape. This option is useful only when escaping
ASTs which may have quoted fragments in them. Defaults to `false`.
ASTs which may have quoted fragments in them. Note this option
will give a special meaning to `quote`/`unquote` nodes, which need
to be valid AST before escaping. Defaults to `false`.
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
nodes. Note this option changes the semantics of escaped code and
it should only be used when escaping ASTs. Defaults to `false`.
* `:generated` - (since v1.19.0) Whether the AST should be considered as generated
by the compiler or not. This means the compiler and tools like Dialyzer may not
emit certain warnings.
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
assertion, so when an assertion fails we can show code snippets to users.
Without this option, each time the test module is compiled, we would get a
@@ -834,12 +848,82 @@ defmodule Macro do
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
expressions.
## Dealing with references and other runtime values
Macros work at compile-time and therefore `Macro.escape/1` can only escape values
that are valid during compilation, such as numbers, atoms, tuples, maps, binaries,
etc.
However, you may have values at compile-time which cannot be escaped, such as
`reference`s and `pid`s, since the process or memory address they point to will
no longer exist once compilation completes. Attempting to escape said values will
raise an exception. This is a common issue when working with NIFs.
Luckily, Elixir v1.19 introduces a mechanism that allows those values to be escaped,
as long as they are encapsulated by a struct within a module that defines the
`__escape__/1` function. This is possible as long as the reference has a natural
text or binary representation that can be serialized during compilation.
Let's imagine we have the following struct:
defmodule WrapperStruct do
defstruct [:ref]
def new(...), do: %WrapperStruct{ref: ...}
# efficiently dump to / load from binaries
def dump_to_binary(%WrapperStruct{ref: ref}), do: ...
def load_from_binary(binary), do: %WrapperStruct{ref: ...}
end
Such a struct could not be used in module attributes or escaped with `Macro.escape/2`:
defmodule Foo do
@my_struct WrapperStruct.new(...)
def my_struct, do: @my_struct
end
** (ArgumentError) cannot inject attribute @my_struct into function/macro because cannot escape #Reference<...>
To address this, structs can re-define how they should be escaped by defining a custom
`__escape__/1` function which returns the AST. In our example:
defmodule WrapperStruct do
# ...
def __escape__(struct) do
# dump to a binary representation at compile-time
binary = dump_to_binary(struct)
quote do
# load from the binary representation at runtime
WrapperStruct.load_from_binary(unquote(Macro.escape(binary)))
end
end
end
Now, our example above will be expanded as:
def my_struct, do: WrapperStruct.load_from_binary(<<...>>)
When implementing `__escape__/1`, you must ensure that the quoted expression
will evaluate to a struct that represents the one given as argument.
"""
@spec escape(term, keyword) :: t()
@spec escape(term, escape_opts) :: t()
def escape(expr, opts \\ []) do
unquote = Keyword.get(opts, :unquote, false)
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
:elixir_quote.escape(expr, kind, unquote)
kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape
generated = Keyword.get(opts, :generated, false)
case :elixir_quote.escape(expr, kind, unquote) do
# mark module attrs as shallow-generated since the ast for their representation
# might contain opaque terms
{caller, meta, args} when generated and is_list(meta) ->
{caller, [generated: true] ++ meta, args}
ast ->
ast
end
end
# TODO: Deprecate me on Elixir v1.22
@@ -852,21 +936,40 @@ defmodule Macro do
end
@doc """
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
Extracts the struct information.
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
not have been compiled (including structs in the defined
under the module being compiled). For compiled modules,
it will invoke `module.__info__(:struct)`.
Calling this function also adds an export dependency on the
given struct.
It will raise `ArgumentError` if the struct is not available.
## Compatibility considerations
This function currently returns both `:required` and `:default`
entries for each field. While this naming is inconsistent
(a required field should not have a default), this is done for
backwards compatibility purposes.
In future releases, Elixir may introduce truly required struct
fields, the required field will be removed and default will be
present only if the field is optional. Your code should prepare
for such scenario accordingly.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
[%{field: atom(), required: boolean(), default: term()}]
[
%{
required(:field) => atom(),
optional(:required) => boolean(),
optional(:default) => term()
}
]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
@@ -1738,12 +1841,17 @@ defmodule Macro do
@doc """
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
This is used when you want to programmatically invoke a macro at
compile-time.
This is used when you want to dynamically invoke a function at
compile-time and force it to be tracked as a compile-time dependency.
For example, this is used by `dbg/1` to force the `dbg_callback`
configuration to be a compile-time dependency.
If you want to "invoke" a macro instead, remember macros are by
definition compile-time, and you can use `Macro.expand/2`.
"""
@doc since: "1.16.0"
def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
Kernel.apply(mod, fun, args)
end
@@ -2399,7 +2507,7 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
@spec inspect_atom(:literal | :key | :remote_call, atom, inspect_atom_opts) :: binary
def inspect_atom(source_format, atom, opts \\ [])
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
@@ -2591,7 +2699,7 @@ defmodule Macro do
:ok
end
prelude = quote do: options = unquote(Macro.escape(options))
prelude = quote do: options = unquote(options)
acc = {prelude, dbg_format_header(env)}
{acc, nil} =
@@ -2612,34 +2720,29 @@ defmodule Macro do
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
[start_ast | rest_asts] = for {ast, 0} <- unpipe(pipe_ast), do: ast
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)})
initial_acc =
first_entry =
quote do
unquote(value_var) = unquote(start_ast)
unquote(values_acc_var) = [unquote(value_var)]
{:multi_value, unquote(escape(start_ast)), unquote(value_var)}
end
values_ast =
for step_ast <- rest_asts, reduce: initial_acc do
ast_acc ->
quote do
unquote(ast_acc)
unquote(value_var) = unquote(step_ast)
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
end
end
len = length(piped_rest_asts)
[
quote do
unquote(values_ast)
pipe_entries =
Enum.with_index(piped_rest_asts, fn {original_ast, step_ast}, i ->
tag = if i + 1 == len, do: :pipe_end, else: :pipe
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
end
]
quote do
unquote(value_var) = unquote(step_ast)
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)}
end
end)
[first_entry | pipe_entries]
end
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
@@ -2859,18 +2962,19 @@ defmodule Macro do
result
end
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
result = List.last(values)
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
[{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do
formatted = [
[:faint, "|> ", :reset],
dbg_format_ast_with_value_no_newline(code_ast, value, options)
]
rest_formatted =
Enum.map(asts_with_values, fn {code_ast, value} ->
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
end)
{formatted, value}
end
{[first_formatted | rest_formatted], result}
defp dbg_format_ast_to_debug({:pipe_end, code_ast, value}, options) do
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
{[formatted, ?\n], value}
end
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
+64 -13
View File
@@ -70,6 +70,42 @@ defmodule Macro.Env do
@typep tracers :: [module]
@typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
@type define_import_opts :: [
trace: boolean(),
emit_warnings: boolean(),
info_callback: (atom() -> [{atom(), arity()}]),
only: :functions | :macros | [{atom(), arity()}],
except: [{atom(), arity()}],
warn: boolean()
]
@type define_alias_opts :: [
trace: boolean(),
as: atom(),
warn: boolean()
]
@type define_require_opts :: [
trace: boolean(),
as: atom(),
warn: boolean()
]
@type expand_alias_opts :: [
trace: boolean()
]
@type expand_import_opts :: [
allow_locals: boolean() | (-> function() | false),
check_deprecations: boolean(),
trace: boolean()
]
@type expand_require_opts :: [
check_deprecations: boolean(),
trace: boolean()
]
@type t :: %{
__struct__: __MODULE__,
aliases: aliases,
@@ -264,7 +300,7 @@ defmodule Macro.Env do
iex> Macro.Env.required?(__ENV__, Integer)
false
iex> require Integer
iex> require Integer, warn: false
iex> Macro.Env.required?(__ENV__, Integer)
true
@@ -331,7 +367,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -391,7 +427,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
@spec define_import(t, Macro.metadata(), module, define_import_opts) ::
{:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -441,7 +478,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
@spec define_alias(t, Macro.metadata(), module, define_alias_opts) ::
{:ok, t} | {:error, String.t()}
def define_alias(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -487,7 +525,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_alias(t, keyword, [atom()], keyword) ::
@spec expand_alias(t, keyword, [atom()], expand_alias_opts) ::
{:alias, atom()} | :error
def expand_alias(env, meta, list, opts \\ [])
when is_list(meta) and is_list(list) and is_list(opts) do
@@ -517,8 +555,15 @@ defmodule Macro.Env do
## Options
* `:allow_locals` - when set to `false`, it does not attempt to capture
local macros defined in the current module in `env`
* `:allow_locals` - controls how local macros are resolved.
Defaults to `true`.
- When `false`, does not attempt to capture local macros defined in the
current module in `env`
- When `true`, uses a default resolver that looks for public macros in
the current module
- When a function, it will be invoked to lazily compute a local function
(or return false). It has signature `(-> function() | false)`
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
@@ -527,7 +572,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
@spec expand_import(t, keyword, atom(), arity(), expand_import_opts) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| {:function, module(), atom()}
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
@@ -542,10 +587,16 @@ defmodule Macro.Env do
trace = Keyword.get(opts, :trace, true)
module = env.module
# When allow_locals is a callback, we don't need to pass module macros as extra
# because the callback will handle local macro resolution
extra =
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
if is_function(allow_locals, 0) do
[]
else
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
end
end
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
@@ -583,7 +634,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
@spec expand_require(t, keyword, module(), atom(), arity(), expand_require_opts) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| :error
def expand_require(env, meta, module, name, arity, opts \\ [])
@@ -606,7 +657,7 @@ defmodule Macro.Env do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(args, env)
quoted = expander.(:elixir_dispatch.stop_generated(args), env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
+91 -49
View File
@@ -287,8 +287,13 @@ defmodule Map do
@doc """
Fetches the value for a specific `key` in the given `map`.
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
If `map` doesn't contain `key`, `:error` is returned.
If `map` contains the given `key` then its value is returned
in the shape of `{:ok, value}`. If `map` doesn't contain `key`,
`:error` is returned.
If the type system can verify `:error` is always returned
(which means key is never available in the map), it will emit
an error.
Inlined by the compiler.
@@ -296,7 +301,7 @@ defmodule Map do
iex> Map.fetch(%{a: 1}, :a)
{:ok, 1}
iex> Map.fetch(%{a: 1}, :b)
iex> Map.fetch(%{"foo" => "bar"}, "unknown")
:error
"""
@@ -307,8 +312,11 @@ defmodule Map do
Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`.
If `map` contains `key`, the corresponding value is returned. If
`map` doesn't contain `key`, a `KeyError` exception is raised.
The exclamation mark (`!`) implies this function can raise a `KeyError`
exception at runtime if `map` doesn't contain `key`. If the type system
can verify this function will always raise (which means the key is never
available), then it will emit a warning at compile-time. See the "Type
checking" section below.
Inlined by the compiler.
@@ -317,11 +325,54 @@ defmodule Map do
iex> Map.fetch!(%{a: 1}, :a)
1
When the key is missing, an exception is raised:
Map.fetch!(%{a: 1}, :b)
** (KeyError) key :b not found in: %{a: 1}
## Type checking
The compiler will emit a warning if it can verify that
none of the keys given are available in the map.
When the key is an atom, because only single key is given,
a warning will be emitted in case the type system proves
the key is not present.
However, this behaviour matters when the type of the key
represents multiple values. For example:
key = returns_foo_or_bar() #=> :foo or :bar
Map.fetch!(%{foo: 123}, key)
Although the key can be `:foo` or `:bar`, there is no
warning emitted, as `:foo` will succeed. This is by design:
the exclamation mark in Elixir denotes precisely that a
runtime exception may be raised.
In case you are looking up multiple keys and you don't know
if they may be present, you can use `Map.fetch/2` instead
and deal with the error case accordingly:
case Map.fetch(%{foo: 123}, key) do
{:ok, value} -> ...
:error -> ...
end
Both `Map.fetch!/2` and `Map.fetch/2` will emit a warning if
it proves that both `:foo` or `:bar` are absent in the map.
Alternatively, if you want to statically prove that all of keys
are in the map, you can match on the possible values and access
them directly:
case returns_foo_or_bar() do
:foo -> map.foo
:bar -> map.bar
end
"""
@spec fetch!(map, key) :: value
def fetch!(map, key) do
:maps.get(key, map)
end
def fetch!(map, key), do: :maps.get(key, map)
@doc """
Puts the given `value` under `key` unless the entry `key`
@@ -357,8 +408,8 @@ defmodule Map do
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace(%{a: 1}, :b, 2)
%{a: 1}
iex> Map.replace(%{"a" => 1}, "b", 2)
%{"a" => 1}
"""
@doc since: "1.11.0"
@@ -379,7 +430,11 @@ defmodule Map do
@doc """
Puts a value under `key` only if the `key` already exists in `map`.
If `key` is not present in `map`, a `KeyError` exception is raised.
The exclamation mark (`!`) implies this function can raise a `KeyError`
exception at runtime if `map` doesn't contain `key`. If the type system
can verify this function will always raise (which means the key is never
available), then it will emit a warning at compile-time. See the "Type
checking" section in `Map.fetch!/2` for more information.
Inlined by the compiler.
@@ -388,8 +443,8 @@ defmodule Map do
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace!(%{a: 1}, :b, 2)
** (KeyError) key :b not found in:
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar")
** (KeyError) key "unknown" not found in:
...
"""
@@ -412,8 +467,8 @@ defmodule Map do
iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end)
%{a: 4, b: 2}
iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
%{a: 1, b: 2}
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end)
%{"a" => 1, "b" => 2}
"""
@doc since: "1.14.0"
@@ -516,15 +571,13 @@ defmodule Map do
## Examples
iex> Map.get(%{}, :a)
nil
iex> Map.get(%{a: 1}, :a)
iex> Map.get(%{"a" => 1}, "a")
1
iex> Map.get(%{a: 1}, :b)
iex> Map.get(%{"a" => 1}, "b")
nil
iex> Map.get(%{a: 1}, :b, 3)
iex> Map.get(%{"a" => 1}, "b", 3)
3
iex> Map.get(%{a: nil}, :a, 1)
iex> Map.get(%{"a" => nil}, "a", 1)
nil
"""
@@ -553,15 +606,11 @@ defmodule Map do
## Examples
iex> map = %{a: 1}
iex> fun = fn ->
...> # some expensive operation here
...> 13
...> end
iex> Map.get_lazy(map, :a, fun)
iex> Map.get_lazy(%{a: 1}, :a, fn -> :expensive_value end)
1
iex> Map.get_lazy(map, :b, fun)
13
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
:expensive_value
"""
@spec get_lazy(map, key, (-> value)) :: value
@@ -700,10 +749,10 @@ defmodule Map do
iex> Map.pop(%{a: 1}, :a)
{1, %{}}
iex> Map.pop(%{a: 1}, :b)
{nil, %{a: 1}}
iex> Map.pop(%{a: 1}, :b, 3)
{3, %{a: 1}}
iex> Map.pop(%{"a" => 1}, "b")
{nil, %{"a" => 1}}
iex> Map.pop(%{"a" => 1}, "b", 3)
{3, %{"a" => 1}}
"""
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
@@ -726,8 +775,8 @@ defmodule Map do
{1, %{}}
iex> Map.pop!(%{a: 1, b: 2}, :a)
{1, %{b: 2}}
iex> Map.pop!(%{a: 1}, :b)
** (KeyError) key :b not found in:
iex> Map.pop!(%{"a" => 1}, "b")
** (KeyError) key "b" not found in:
...
"""
@@ -753,15 +802,11 @@ defmodule Map do
## Examples
iex> map = %{a: 1}
iex> fun = fn ->
...> # some expensive operation here
...> 13
...> end
iex> Map.pop_lazy(map, :a, fun)
iex> Map.pop_lazy(%{a: 1}, :a, fn -> :expensive_value end)
{1, %{}}
iex> Map.pop_lazy(map, :b, fun)
{13, %{a: 1}}
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
{:expensive_value, %{"a" => 1}}
"""
@spec pop_lazy(map, key, (-> value)) :: {value, map}
@@ -913,8 +958,8 @@ defmodule Map do
iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
%{a: 2}
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
** (KeyError) key :b not found in:
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2))
** (KeyError) key "b" not found in:
...
"""
@@ -1033,6 +1078,7 @@ defmodule Map do
#=> %{name: "john"}
"""
# TODO: implement this using row polymorphism
@spec from_struct(atom | struct) :: map
def from_struct(struct) when is_atom(struct) do
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
@@ -1069,11 +1115,7 @@ defmodule Map do
"""
@spec equal?(map, map) :: boolean
def equal?(map1, map2)
def equal?(%{} = map1, %{} = map2), do: map1 === map2
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
@doc false
@deprecated "Use Kernel.map_size/1 instead"
+4 -1
View File
@@ -55,7 +55,10 @@ defmodule MapSet do
@type value :: term
@opaque internal(value) :: :sets.set(value)
# We don't use opaque because MapSets can be inlined,
# either via module attributes or by the compiler.
@typep internal(value) :: :sets.set(value)
@type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term)
+38 -25
View File
@@ -402,30 +402,21 @@ defmodule Module do
Accepts the function name (as an atom) of a function in the current module.
The function must have an arity of 0 (no arguments). If the function does
not return `:ok`, the loading of the module will be aborted.
For example:
not return `:ok`, the loading of the module will be aborted. Its primary
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
defmodule MyModule do
@on_load :load_check
@on_load :load_external_code
def load_check do
if some_condition() do
:ok
else
:abort
end
end
def some_condition do
false
def load_external_code do
:erlang.load_nif(~c"path/to/extension.so_or_dll")
end
end
The function given to `on_load` should avoid calling functions from
other modules. If you must call functions in other modules and those
modules are defined within the same project, the called modules must
have the `@compile {:autoload, true}` annotation, so they are loaded
upfront (and not from within the `@on_load` callback).
other modules. This is because, when running a `mix release`,
`on_load` runs extremely early, before any application starts running,
and therefore even systems like the `Logger` and `IO` are not yet available.
### `@vsn`
@@ -560,15 +551,20 @@ defmodule Module do
callback is invoked under different scenarios, Elixir provides no guarantees
of when in the compilation cycle nor in which process the callback runs.
Furthermore, after verification callbacks are not expected to raise.
Given they run after the code is compiled, artifacts have already been
written to disk, and therefore raising does not effectively halt compilation
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
or other callback. Given modules have already been compiled, functions in
this module, such as `get_attribute/2`, which expect modules to not have been
yet compiled, do not work on `@after_verify` callback.
Accepts a module or a `{module, function_name}` tuple. The function
must take one argument: the module name. When just a module is provided,
the function is assumed to be `__after_verify__/1`.
Callbacks will run in the order they are registered.
`Module` functions expecting not yet compiled modules are no longer available
at the time `@after_verify` is invoked.
#### Example
defmodule MyModule do
@@ -681,12 +677,21 @@ defmodule Module do
This function is generated for all modules. It's similar to `module_info/1` but
includes some additional Elixir-specific information, such as struct and macro
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@type def_kind :: :def | :defp | :defmacro | :defmacrop
@type create_opts :: [
file: binary(),
line: pos_integer(),
generated: boolean()
]
@type get_definition_opts :: [
skip_clauses: boolean()
]
@extra_error_msg_defines? "Use Kernel.function_exported?/3 and Kernel.macro_exported?/3 " <>
"to check for public functions and macros instead"
@@ -711,7 +716,8 @@ defmodule Module do
* `:module` - the module atom name
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order.
See `Macro.struct_info!/2` for more information
"""
@callback __info__(:attributes) :: keyword()
@@ -721,7 +727,14 @@ defmodule Module do
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
[
%{
required(:field) => atom(),
optional(:required) => boolean(),
optional(:default) => term()
}
]
| nil
@doc """
Returns information about module attributes used by Elixir.
@@ -918,7 +931,7 @@ defmodule Module do
when defining the module, while `Kernel.defmodule/2`
automatically uses the environment it is invoked at.
"""
@spec create(module, Macro.t(), Macro.Env.t() | keyword) :: {:module, module, binary, term}
@spec create(module, Macro.t(), Macro.Env.t() | create_opts) :: {:module, module, binary, term}
def create(module, quoted, opts)
def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do
@@ -1423,7 +1436,7 @@ defmodule Module do
only an interest in fetching the kind and the metadata
"""
@spec get_definition(module, definition, keyword) ::
@spec get_definition(module, definition, get_definition_opts) ::
{:v1, def_kind, meta :: keyword,
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil
+28 -4
View File
@@ -181,8 +181,16 @@ defmodule Module.Behaviour do
behaviour not in behaviours ->
{:error, {:behaviour_not_declared, behaviour}}
not Code.ensure_loaded?(behaviour) ->
# Module does not exist, but we have already warned about it.
{:ok, []}
not behaviour_defined?(callbacks, behaviour) ->
# Module does not define behaviour, but we have already warned about it.
{:ok, []}
true ->
{:error, {:behaviour_not_defined, behaviour, callbacks}}
{:error, {:callback_not_defined, behaviour, callbacks}}
end
end
@@ -215,6 +223,18 @@ defmodule Module.Behaviour do
end
end
# Determines whether there is at least one callback defined for the given behaviour.
# If not, that means that the behaviour has not been defined.
defp behaviour_defined?(callbacks, behaviour) do
callbacks
|> Map.values()
|> List.flatten()
|> Enum.any?(fn
{_kind, ^behaviour, _optional?} -> true
{_kind, _behaviour, _optional?} -> false
end)
end
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
when map_size(callbacks) == 0 do
context
@@ -390,13 +410,17 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do
behaviour_string = inspect(behaviour)
[
"got \"@impl ",
inspect(behaviour),
behaviour_string,
"\" for ",
format_definition(kind, callback),
" but this behaviour does not specify such callback",
" but ",
behaviour_string,
" does not specify such callback",
known_callbacks(callbacks)
]
end
+26 -20
View File
@@ -4,6 +4,7 @@
defmodule Module.ParallelChecker do
@moduledoc false
@elixir_checker_version :elixir_erl.checker_version()
import Kernel, except: [spawn: 3]
@@ -11,9 +12,20 @@ defmodule Module.ParallelChecker do
@type warning() :: term()
@type mode() :: :erlang | :elixir | :protocol
@typedoc """
Options for `start_link/1`.
"""
@type start_link_opts :: [
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {atom(), term()}
]
@doc """
Initializes the parallel checker process.
"""
@spec start_link(start_link_opts()) :: {:ok, cache()}
def start_link(opts \\ []) do
:proc_lib.start_link(__MODULE__, :init, [opts])
end
@@ -51,14 +63,14 @@ defmodule Module.ParallelChecker do
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) do
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
# Protocols may have been consolidated. So if we know their beam location,
# we discard their module map on purpose and start from file.
info =
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
List.to_string(beam_location)
else
cache_from_module_map(table, module_map)
cache_from_module_map(table, module_map, signatures)
end
inner_spawn(pid, checker, table, module, info, log?)
@@ -85,10 +97,12 @@ defmodule Module.ParallelChecker do
end
with {:ok, binary} <- File.read(location),
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
cache_from_module_map(table, module_map)
{:ok,
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:not_found, nil}
end
@@ -166,9 +180,9 @@ defmodule Module.ParallelChecker do
end
@doc """
Receives pairs of module maps and BEAM binaries. In parallel it verifies
the modules and adds the ExCk chunk to the binaries. Returns the updated
list of warnings from the verification.
Receives pairs of module maps and BEAM binaries.
Returns the updated list of warnings from the verification.
"""
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
def verify({checker, table}, runtime_files) do
@@ -206,14 +220,6 @@ defmodule Module.ParallelChecker do
end
end
@doc """
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
end
@doc """
Returns the export kind and deprecation reason for the given MFA from
the cache. If the module does not exist return `:badmodule`,
@@ -412,7 +418,7 @@ defmodule Module.ParallelChecker do
mode =
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
# The chunk has more information, so that's our preference
cache_chunk(table, module, contents)
else
@@ -463,12 +469,12 @@ defmodule Module.ParallelChecker do
if Keyword.has_key?(attributes, :__protocol__), do: :protocol, else: :elixir
end
defp cache_from_module_map(table, map) do
defp cache_from_module_map(table, map, signatures) do
exports =
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures)
cache_info(table, map.module, exports, Map.new(map.deprecated), signatures)
{elixir_mode(map.attributes), module_map_to_module_tuple(map)}
end
+74 -56
View File
@@ -24,20 +24,17 @@ defmodule Module.Types do
#
# * :infer - Same as :dynamic but skips remote calls.
#
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
@modes [:static, :dynamic, :infer, :traversal]
@modes [:static, :dynamic, :infer]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [behaviour_info: 1]
@doc false
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do
# We don't care about inferring signatures for protocols,
# those will be replaced anyway. There is also nothing to
# infer if there is no cache system, we only do traversals.
@@ -49,8 +46,8 @@ defmodule Module.Types do
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause ->
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
{_, kind, _, _} = def ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
false ->
false
@@ -75,23 +72,27 @@ defmodule Module.Types do
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
{types, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- defs,
kind in [:def, :defmacro],
reduce: {[], context()} do
{types, context} ->
# Optimized version of finder, since we already the definition
# In case there are loops, the other we traverse matters,
# so we sort the definitions for determinism
{types, private, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
reduce: {[], [], context()} do
{types, private, context} when kind in [:def, :defmacro] ->
# Optimized version of finder, since we already have the definition
finder = fn _ ->
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], context}
{[{fun_arity, inferred} | types], private, context}
else
{types, context}
{types, private, context}
end
{types, private, context} ->
{types, [def | private], context}
end
# Now traverse all used privates to find any other private that have been used by them.
@@ -105,8 +106,8 @@ defmodule Module.Types do
{unreachable, _context} =
Enum.reduce(private, {[], context}, fn
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
warn_unused_def(info, used_sigs, env)
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
# Find anything undefined within unused functions
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
@@ -125,7 +126,7 @@ defmodule Module.Types do
end
defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse
end
defp protocol?(attrs) do
@@ -146,12 +147,24 @@ defmodule Module.Types do
end
end
defp default_domain({_, arity} = fun_arity, impl) do
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
with {for, callbacks} <- impl,
true <- fun_arity in callbacks do
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
args = [
Descr.dynamic(Module.Types.Of.impl(for))
| List.duplicate(Descr.dynamic(), arity - 1)
]
{_fun_arity, kind, meta, clauses} = def
clauses =
for {meta, args, guards, body} <- clauses do
{[type_check: {:impl, for}] ++ meta, args, guards, body}
end
{mode, {fun_arity, kind, meta, clauses}, args}
else
_ -> List.duplicate(Descr.dynamic(), arity)
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
end
end
@@ -161,29 +174,30 @@ defmodule Module.Types do
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
:ok
end
defp warn_unused_def(fun_arity, kind, meta, used, env) do
default = Keyword.get(meta, :defaults, 0)
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
cond do
Keyword.get(meta, :context) != nil ->
:ok
:ok
end
default == 0 ->
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
default > 0 ->
{name, arity} = fun_arity
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
end
end
:ok
@@ -208,7 +222,7 @@ defmodule Module.Types do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
false -> false
end
end
@@ -219,7 +233,7 @@ defmodule Module.Types do
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already the definition
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
@@ -279,7 +293,7 @@ defmodule Module.Types do
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -292,7 +306,17 @@ defmodule Module.Types do
end
end
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
context =
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
Module.Types.Traverse.of_expr(body, stack, context)
end)
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
{fun, _arity} = fun_arity
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
@@ -308,12 +332,7 @@ defmodule Module.Types do
{return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, context)
args_types =
if stack.mode == :traversal do
expected
else
Pattern.of_domain(trees, expected, context)
end
args_types = Pattern.of_domain(trees, context)
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
@@ -416,10 +435,7 @@ defmodule Module.Types do
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler,
# Control if variable refinement is enabled.
# It is disabled only on dynamic dispatches.
refine_vars: true
local_handler: handler
}
end
@@ -430,7 +446,9 @@ defmodule Module.Types do
warnings: [],
# All vars and their types
vars: %{},
# Variables and arguments from patterns
# Variables that are specific to the current environment/conditional
conditional_vars: nil,
# Track metadata specific to matches and guards
pattern_info: nil,
# If type checking has found an error/failure
failed: false,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+263 -190
View File
@@ -15,6 +15,7 @@ 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]),
@@ -34,9 +35,12 @@ defmodule Module.Types.Expr do
versioned_vars: open_map()
)
# An annotation for terms where the reverse arrow is not yet fully defined
# 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()
@atom_true atom([true])
# 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)
args_or_arity = union(list(term()), integer())
@@ -83,34 +87,28 @@ defmodule Module.Types.Expr do
def of_expr(list, expected, expr, stack, context) when is_list(list) do
{prefix, suffix} = unpack_list(list, [])
if stack.mode == :traversal do
{_, context} = Enum.map_reduce(prefix, context, &of_expr(&1, term(), expr, stack, &2))
{_, context} = of_expr(suffix, term(), expr, stack, context)
{dynamic(), context}
else
hd_type =
case list_hd(expected) do
{_, type} -> type
_ -> term()
end
hd_type =
case list_hd(expected) do
{:ok, type} -> type
_ -> term()
end
{prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, hd_type, expr, stack, &2))
{prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, hd_type, expr, stack, &2))
{suffix, context} =
if suffix == [] do
{empty_list(), context}
else
tl_type =
case list_tl(expected) do
{_, type} -> type
_ -> term()
end
{suffix, context} =
if suffix == [] do
{empty_list(), context}
else
tl_type =
case list_tl(expected) do
{:ok, type} -> type
:badnonemptylist -> term()
end
of_expr(suffix, tl_type, expr, stack, context)
end
of_expr(suffix, tl_type, expr, stack, context)
end
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
end
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
end
# {left, right}
@@ -125,8 +123,9 @@ defmodule Module.Types.Expr do
# <<...>>>
def of_expr({:<<>>, _meta, args}, _expected, _expr, stack, context) do
context = Of.binary(args, :expr, stack, context)
{binary(), context}
args
|> Of.bitstring(:expr, stack, context)
|> dynamic_unless_static(stack)
end
def of_expr({:__CALLER__, _meta, var_context}, _expected, _expr, _stack, context)
@@ -168,57 +167,32 @@ defmodule Module.Types.Expr do
# allow variables defined on the left side of | to be available
# on the right side, this is safe.
{pairs_types, context} =
Of.pairs(args, expected, stack, context, &of_expr(&1, &2, expr, &3, &4))
Enum.map_reduce(args, context, fn {key, value}, context ->
{key_type, context} = of_expr(key, term(), expr, stack, context)
{value_type, context} = of_expr(value, term(), expr, stack, context)
{{key_type, value_type}, context}
end)
expected =
if stack.mode == :traversal do
expected
else
# TODO: Once we introduce domain keys, if we ever find a domain
# that overlaps atoms, we can only assume optional(atom()) => term(),
# which is what the `open_map()` below falls back into anyway.
Enum.reduce_while(pairs_types, expected, fn
{_, [key], _}, acc ->
case map_fetch_and_put(acc, key, term()) do
{_value, acc} -> {:cont, acc}
_ -> {:halt, open_map()}
end
_, _ ->
{:halt, open_map()}
end)
end
# The only information we can attach to the expected types is that
# certain keys are expected.
expected_pairs =
Enum.flat_map(pairs_types, fn {key_type, _value_type} ->
case atom_fetch(key_type) do
{:finite, [key]} -> [{key, term()}]
_ -> []
end
end)
expected = intersection(expected, open_map(expected_pairs))
{map_type, context} = of_expr(map, expected, expr, stack, context)
try do
Of.permutate_map(pairs_types, stack, fn fallback, keys_to_assert, pairs ->
# Ensure all keys to assert and all type pairs exist in map
keys_to_assert = Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert
Enum.each(Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert, fn key ->
case map_fetch(map_type, key) do
{_, _} -> :ok
:badkey -> throw({:badkey, map_type, key, update, context})
:badmap -> throw({:badmap, map_type, update, context})
end
end)
# If all keys are known is no fallback (i.e. we know all keys being updated),
# we can update the existing map.
if fallback == none() do
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
case map_fetch_and_put(acc, key, type) do
{_value, descr} -> descr
:badkey -> throw({:badkey, map_type, key, update, context})
:badmap -> throw({:badmap, map_type, update, context})
end
end)
else
# TODO: Use the fallback type to actually indicate if open or closed.
# The fallback must be unioned with the result of map_values with all
# `keys` deleted.
dynamic(open_map(pairs))
Enum.reduce(pairs_types, map_type, fn {key_type, value_type}, acc ->
case literal_map_update(acc, key_type, value_type) do
{:ok, descr} -> descr
{:badkey, key} -> throw({:badkey, map_type, key, update, context})
{:baddomain, domain} -> throw({:baddomain, map_type, domain, update, context})
:badmap -> throw({:badmap, map_type, update, context})
end
end)
catch
@@ -229,23 +203,35 @@ defmodule Module.Types.Expr do
end
# %Struct{map | ...}
# This syntax is deprecated, so we simply traverse.
def of_expr(
{:%, _, [_, {:%{}, _, [{:|, _, [map, args]}]}]} = struct,
{:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]} = struct,
_expected,
expr,
stack,
context
) do
{_, context} = of_expr(map, term(), struct, stack, context)
# We pass the expected type as `term()` because the struct update
# operator already expects it to be a map at this point.
{map_type, context} = of_expr(map, term(), struct, stack, context)
context =
Enum.reduce(args, context, fn {key, value}, context when is_atom(key) ->
{_, context} = of_expr(value, term(), expr, stack, context)
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
{:finite, [^module]} <- atom_fetch(struct_key_type) do
context
end)
else
_ ->
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
end
{dynamic(), context}
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
# TODO: Once we support typed structs, we need to type check them here
{type, context} = of_expr(value, term(), expr, stack, context)
case map_put_key(acc, key, type) do
{:ok, acc} -> {acc, context}
_ -> {acc, context}
end
end)
end
# %{...}
@@ -302,7 +288,7 @@ defmodule Module.Types.Expr do
end
{body_type, context} = of_expr(body, expected, expr, stack, context)
{union(body_type, acc), reset_vars(context, original)}
{union(body_type, acc), Of.reset_vars(context, original)}
end)
|> dynamic_unless_static(stack)
end
@@ -311,10 +297,17 @@ defmodule Module.Types.Expr do
{case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context)
info = {:case, meta, case_type, case_expr}
# If we are only type checking the expression and the expression is a literal,
# let's mark it as generated, as it is most likely a macro code. However, if
# no clause is matched, we should still check for that.
if Macro.quoted_literal?(case_expr) do
added_meta =
if Macro.quoted_literal?(case_expr) do
[generated: true]
else
case_expr |> get_meta() |> Keyword.take([:generated])
end
# 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
@@ -323,25 +316,20 @@ defmodule Module.Types.Expr do
|> dynamic_unless_static(stack)
end
# TODO: fn pat -> expr 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)
if stack.mode == :traversal do
{_acc, context} = of_clauses(clauses, domain, @pending, nil, :fn, stack, context, none())
{dynamic(fun(length(patterns))), context}
else
{acc, context} =
of_clauses_fun(clauses, domain, @pending, nil, :fn, stack, context, [], fn
trees, body, context, acc ->
args = Pattern.of_domain(trees, domain, context)
add_inferred(acc, args, body)
end)
{acc, context} =
of_clauses_fun(clauses, domain, @pending, nil, :fn, stack, context, [], fn
trees, body, context, acc ->
args = Pattern.of_domain(trees, context)
add_inferred(acc, args, body)
end)
{fun_from_overlapping_clauses(acc), context}
end
{fun_from_inferred_clauses(acc), context}
end
def of_expr({:try, _meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
@@ -359,7 +347,7 @@ defmodule Module.Types.Expr do
{type, context} =
blocks
|> Enum.reduce({type, reset_vars(context, original)}, fn
|> 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} ->
@@ -405,7 +393,7 @@ defmodule Module.Types.Expr do
{body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do
{union(body_type, acc), reset_vars(context, original)}
{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)}
@@ -429,20 +417,26 @@ defmodule Module.Types.Expr do
else
# TODO: Use the collectable protocol for the output
into = Keyword.get(opts, :into, [])
{into_wrapper, gradual?, context} = for_into(into, meta, stack, context)
{into_type, into_kind, context} = for_into(into, meta, stack, context)
{block_type, context} = of_expr(block, @pending, block, stack, context)
for_type =
for type <- into_wrapper do
case type do
:binary -> binary()
:list -> list(block_type)
:term -> term()
end
end
|> Enum.reduce(&union/2)
case into_kind do
:bitstring ->
case compatible_intersection(block_type, bitstring()) do
{:ok, intersection} ->
{return_union(into_type, intersection, stack), context}
{if(gradual?, do: dynamic(for_type), else: for_type), context}
{:error, _} ->
error = {:badbitbody, block_type, block, context}
{error_type(), error(__MODULE__, error, meta, stack, context)}
end
:non_empty_list ->
{return_union(into_type, non_empty_list(block_type), stack), context}
:none ->
{into_type, context}
end
end
end
@@ -461,7 +455,7 @@ defmodule Module.Types.Expr do
{args_types, context} =
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
Apply.fun_apply(fun_type, args_types, call, stack, context)
Apply.fun(fun_type, args_types, call, stack, context)
end
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
@@ -482,7 +476,7 @@ defmodule Module.Types.Expr do
apply_many(mods, name, args, expected, call, stack, context)
end
# TODO: &Foo.bar/1
# &Foo.bar/1
def of_expr(
{:&, _, [{:/, _, [{{:., _, [remote, name]}, meta, []}, arity]}]} = call,
_expected,
@@ -496,7 +490,7 @@ defmodule Module.Types.Expr do
Apply.remote_capture(mods, name, arity, meta, stack, context)
end
# TODO: &foo/1
# &foo/1
def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, _expected, _expr, stack, context) do
Apply.local_capture(fun, arity, meta, stack, context)
end
@@ -504,31 +498,24 @@ defmodule Module.Types.Expr do
# Super
def of_expr({:super, meta, args} = call, expected, _expr, stack, context) when is_list(args) do
{_kind, fun} = Keyword.fetch!(meta, :super)
apply_local(fun, args, expected, call, stack, context)
Apply.local(fun, args, expected, call, stack, context, &of_expr/5)
end
# Local calls
def of_expr({fun, _meta, args} = call, expected, _expr, stack, context)
when is_atom(fun) and is_list(args) do
apply_local(fun, args, expected, call, stack, context)
Apply.local(fun, args, expected, call, stack, context, &of_expr/5)
end
# var
def of_expr(var, expected, expr, stack, context) when is_var(var) do
case stack do
%{mode: :traversal} -> {dynamic(), context}
%{refine_vars: false} -> {Of.var(var, context), context}
%{} -> Of.refine_body_var(var, expected, expr, stack, context)
end
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)}
end
## Tuples
defp of_tuple(elems, _expected, expr, %{mode: :traversal} = stack, context) do
{_types, context} = Enum.map_reduce(elems, context, &of_expr(&1, term(), expr, stack, &2))
{dynamic(), context}
end
defp of_tuple(elems, expected, expr, stack, context) do
of_tuple(elems, 0, [], expected, expr, stack, context)
end
@@ -575,12 +562,13 @@ defmodule Module.Types.Expr do
_ ->
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, reset_vars(context, original)}
{type, Of.reset_vars(context, original)}
end
## Comprehensions
@@ -590,19 +578,19 @@ defmodule Module.Types.Expr do
{pattern, guards} = extract_head([left])
{_type, context} =
apply_one(Enumerable, :count, [right], dynamic(), expr, stack, context)
Apply.remote(Enumerable, :count, [right], dynamic(), expr, stack, context, &of_expr/5)
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context)
end
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do
{right_type, context} = of_expr(right, binary(), expr, stack, context)
context = Pattern.of_generator(left, [], binary(), :for, expr, stack, context)
{right_type, context} = of_expr(right, bitstring(), expr, stack, context)
context = Pattern.of_generator(left, [], bitstring(), :for, expr, stack, context)
if compatible?(right_type, binary()) do
if compatible?(right_type, bitstring()) do
context
else
error = {:badbinary, right_type, right, context}
error = {:badbitgenerator, right_type, right, context}
error(__MODULE__, error, meta, stack, context)
end
end
@@ -612,13 +600,13 @@ defmodule Module.Types.Expr do
context
end
@into_compile union(binary(), empty_list())
@into_compile union(bitstring(), empty_list())
defp for_into([], _meta, _stack, context),
do: {[:list], false, context}
do: {empty_list(), :non_empty_list, context}
defp for_into(binary, _meta, _stack, context) when is_binary(binary),
do: {[:binary], false, context}
do: {binary(), :bitstring, context}
defp for_into(into, meta, stack, context) do
meta =
@@ -635,21 +623,33 @@ defmodule Module.Types.Expr do
{type, context} = of_expr(into, domain, expr, stack, context)
# We use subtype? instead of compatible because we want to handle
# only binary/list, even if a dynamic with something else is given.
# only bitstring/list, even if a dynamic with something else is given.
if subtype?(type, @into_compile) do
case {binary_type?(type), empty_list_type?(type)} do
{false, true} -> {[:list], gradual?(type), context}
{true, false} -> {[:binary], gradual?(type), context}
{_, _} -> {[:binary, :list], gradual?(type), context}
case {bitstring_type?(type), empty_list_type?(type)} do
# If they can be both be true, then we don't know
# what the contents of the block are for
{true, true} ->
type = union(bitstring(), list(term()))
{if(gradual?(type), do: dynamic(type), else: type), :none, context}
{false, true} ->
{type, :non_empty_list, context}
{true, false} ->
{type, :bitstring, context}
end
else
{_type, context} =
Apply.remote_apply(info, Collectable, :into, [type], expr, stack, context)
{[:term], true, context}
{dynamic(), :none, context}
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
@@ -665,7 +665,7 @@ defmodule Module.Types.Expr do
defp with_option({:do, body}, stack, context, original) do
{_type, context} = of_expr(body, @pending, body, stack, context)
reset_vars(context, original)
Of.reset_vars(context, original)
end
defp with_option({:else, clauses}, stack, context, _original) do
@@ -677,46 +677,22 @@ defmodule Module.Types.Expr do
## General helpers
defp apply_local(fun, args, expected, {_, meta, _} = expr, stack, context) do
{local_info, domain, context} = Apply.local_domain(fun, args, expected, meta, stack, context)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3))
Apply.local_apply(local_info, fun, args_types, expr, stack, context)
end
defp apply_one(mod, fun, args, expected, expr, stack, context) do
{info, domain, context} =
Apply.remote_domain(mod, fun, args, expected, elem(expr, 1), stack, context)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3))
Apply.remote_apply(info, mod, fun, args_types, expr, stack, context)
end
defp apply_many([], fun, args, expected, expr, stack, context) do
{info, domain} = Apply.remote_domain(fun, args, expected, stack)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3))
Apply.remote_apply(info, nil, fun, args_types, expr, stack, context)
Apply.remote(fun, args, expected, expr, stack, context, &of_expr/5)
end
defp apply_many([mod], fun, args, expected, expr, stack, context) do
apply_one(mod, fun, args, expected, expr, stack, context)
Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
end
defp apply_many(mods, fun, args, expected, {remote, meta, args}, stack, context) do
{returns, context} =
Enum.map_reduce(mods, context, fn mod, context ->
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
apply_one(mod, fun, args, expected, expr, %{stack | refine_vars: false}, context)
end)
defp apply_many(mods, fun, args, expected, call, stack, context) do
{remote, meta, _} = call
{Enum.reduce(returns, &union/2), context}
Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, context ->
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
{type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
{union(acc, type), context}
end)
end
defp reduce_non_empty([last], acc, fun),
@@ -728,14 +704,8 @@ defmodule Module.Types.Expr do
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
defp of_clauses(clauses, domain, expected, expr, info, %{mode: mode} = stack, context, acc) do
fun =
if mode == :traversal do
fn _, _, _, _ -> dynamic() end
else
fn _trees, result, _context, acc -> union(result, acc) end
end
defp of_clauses(clauses, domain, expected, expr, info, stack, context, acc) do
fun = fn _trees, result, _context, acc -> union(result, acc) end
of_clauses_fun(clauses, domain, expected, expr, info, stack, context, acc, fun)
end
@@ -748,7 +718,9 @@ defmodule Module.Types.Expr do
{trees, context} = Pattern.of_head(patterns, guards, domain, info, meta, stack, context)
{result, context} = of_expr(body, expected, expr || body, stack, context)
{fun.(trees, result, context, acc), context |> set_failed(failed?) |> reset_vars(original)}
{fun.(trees, result, context, acc),
context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
end
@@ -758,8 +730,6 @@ defmodule Module.Types.Expr do
defp set_failed(%{failed: false} = context, true), do: %{context | failed: true}
defp set_failed(context, _bool), do: context
defp reset_vars(context, %{vars: vars}), do: %{context | vars: vars}
defp extract_head([{:when, _meta, args}]) do
case Enum.split(args, -1) do
{patterns, [guards]} -> {patterns, flatten_when(guards)}
@@ -789,8 +759,69 @@ defmodule Module.Types.Expr do
defp add_inferred([], args, return),
do: [{args, return}]
defp literal_map_update(descr, key_descr, value_descr) do
case map_update(descr, key_descr, value_descr, false, false) do
{_type, descr, []} -> {:ok, descr}
{_, _, [error | _]} -> error
:badmap -> :badmap
{:error, [error | _]} -> error
{:error, []} -> {:baddomain, key_descr}
end
end
## Warning formatting
def format_diagnostic({:badupdate, type, expr, context}) do
{:%, _, [module, {:%{}, _, [{:|, _, [map, _]}]}]} = expr
traces = collect_traces(map, context)
fix =
case map do
{var, meta, context} when is_atom(var) and is_atom(context) ->
if capture = meta[:capture] do
"instead of using &#{capture}, you must define an anonymous function, define a variable and pattern match on \"%#{inspect(module)}{}\""
else
"when defining the variable \"#{Macro.to_string(map)}\", you must also pattern match on \"%#{inspect(module)}{}\""
end
_ ->
"you must assign \"#{Macro.to_string(map)}\" to variable and pattern match on \"%#{inspect(module)}{}\""
end
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
a struct for #{inspect(module)} is expected on struct update:
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
but got type:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces),
"""
#{fix}.
#{hint()} given pattern matching is enough to catch typing errors, \
you may optionally convert the struct update into a map update. For \
example, instead of:
user = some_function()
%User{user | name: "John Doe"}
it is enough to write:
%User{} = user = some_function()
%{user | name: "John Doe"}
"""
])
}
end
def format_diagnostic({:badmap, type, expr, context}) do
traces = collect_traces(expr, context)
@@ -833,7 +864,7 @@ defmodule Module.Types.Expr do
}
end
def format_diagnostic({:badbinary, type, expr, context}) do
def format_diagnostic({:baddomain, type, key_type, expr, context}) do
traces = collect_traces(expr, context)
%{
@@ -841,7 +872,49 @@ defmodule Module.Types.Expr do
message:
IO.iodata_to_binary([
"""
expected the right side of <- in a binary generator to be a binary:
expected a map with key of type #{to_quoted_string(key_type)} in map update syntax:
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
but got type:
#{to_quoted_string(type, collapse_structs: false) |> indent(4)}
""",
format_traces(traces)
])
}
end
def format_diagnostic({:badbitgenerator, type, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected the right side of <- in a binary generator to be a binary (or bitstring):
#{expr_to_string(expr) |> indent(4)}
but got type:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces)
])
}
end
def format_diagnostic({:badbitbody, type, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected the body of a for-comprehension with into: binary() (or bitstring()) to be a binary (or bitstring):
#{expr_to_string(expr) |> indent(4)}
+36 -26
View File
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
@doc """
Returns true if the mode cares about warnings.
"""
defguard is_warning(stack) when stack.mode not in [:traversal, :infer]
defguard is_warning(stack) when stack.mode != :infer
@doc """
Guard function to check if an AST node is a variable.
@@ -91,29 +91,6 @@ defmodule Module.Types.Helpers do
"var.fun()" (with parentheses) means "var" is an atom()
"""
:interpolation ->
"""
#{hint()} string interpolation uses the String.Chars protocol to \
convert a data structure into a string. Either convert the data type into a \
string upfront or implement the protocol accordingly
"""
:generator ->
"""
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
data structures. Either convert the data type into a list (or another Enumerable) \
or implement the protocol accordingly
"""
:into ->
"""
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
build its result. Either pass a valid data type or implement the protocol accordingly
"""
:anonymous_rescue ->
"""
@@ -132,6 +109,20 @@ defmodule Module.Types.Helpers do
the union (which may be none)
"""
{:impl, for} ->
# Get the type without dynamic for better pretty printing
type =
for
|> Module.Types.Of.impl()
|> Module.Types.Descr.dynamic()
|> Map.fetch!(:dynamic)
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
"""
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
"""
:empty_domain ->
"""
@@ -141,7 +132,8 @@ defmodule Module.Types.Helpers do
end)
end
defp hint, do: :elixir_errors.prefix(:hint)
@doc "The hint prefix"
def hint, do: :elixir_errors.prefix(:hint)
@doc """
Collect traces from variables in expression.
@@ -156,7 +148,7 @@ defmodule Module.Types.Helpers do
version = meta[:version]
case vars do
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
@@ -270,6 +262,10 @@ defmodule Module.Types.Helpers do
translating inlined Erlang calls back to Elixir.
We also undo some macro expressions done by the Kernel module.
## Options
* `:collapse_structs` - when false, show structs full representation
"""
def expr_to_string(expr, opts \\ []) do
string = prewalk_expr_to_string(expr, opts)
@@ -340,6 +336,13 @@ defmodule Module.Types.Helpers do
{{:., _, [mod, fun]}, meta, args} ->
erl_to_ex(mod, fun, args, meta)
{:fn, meta, [{:->, _, [_args, return]}]} = expr ->
if meta[:capture] do
{:&, meta, [return]}
else
expr
end
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} ->
{mod, fun} =
case :elixir_rewrite.erl_to_ex(mod, fun, arity) do
@@ -385,6 +388,13 @@ defmodule Module.Types.Helpers do
case
end
{var, meta, context} = expr when is_atom(var) and is_atom(context) ->
if is_integer(meta[:capture]) do
{:&, meta, [meta[:capture]]}
else
expr
end
other ->
other
end)
+337 -182
View File
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
@@ -11,10 +12,10 @@ defmodule Module.Types.Of do
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer()
@float float()
@binary binary()
@bitstring bitstring()
## Variables
@@ -29,19 +30,53 @@ defmodule Module.Types.Of do
@doc """
Marks a variable with error.
This purposedly deletes all traces of the variable,
as it is often invoked when the cause for error is elsewhere.
"""
def error_var(var, context) do
def error_var({_, meta, _}, context) do
error_var(Keyword.fetch!(meta, :version), context)
end
def error_var(version, context) do
update_in(context.vars[version], fn
%{errored: true} = data -> data
data -> Map.put(%{data | type: error_type(), off_traces: []}, :errored, true)
end)
end
@doc """
Declares a variable.
"""
def declare_var(var, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
data = %{
type: error_type(),
name: var_name,
context: var_context,
off_traces: []
}
case context.vars do
%{^version => _} ->
context
put_in(context.vars[version], data)
vars ->
data = %{
type: term(),
name: var_name,
context: var_context,
off_traces: [],
paths: [],
deps: %{}
}
%{context | vars: Map.put(vars, version, data)}
end
end
@doc """
Tracks metadata about variables dependencies and paths.
"""
def track_var(version, new_deps, new_paths, context) do
update_in(context.vars[version], fn %{paths: paths, deps: deps} = data ->
%{data | paths: new_paths ++ paths, deps: Enum.reduce(new_deps, deps, &Map.put(&2, &1, []))}
end)
end
@doc """
@@ -52,10 +87,23 @@ defmodule Module.Types.Of do
Returns `true` if there was a refinement, `false` otherwise.
"""
def refine_body_var({_, meta, _}, type, expr, stack, context) do
version = Keyword.fetch!(meta, :version)
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_body_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
if gradual?(old_type) and type not in [term(), dynamic()] do
context =
case context.conditional_vars do
%{} = conditional_vars ->
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
nil ->
context
end
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
case compatible_intersection(old_type, type) do
{:ok, new_type} when new_type != old_type ->
data = %{
@@ -81,11 +129,16 @@ defmodule Module.Types.Of do
because we want to refine types. Otherwise we should
use compatibility.
"""
def refine_head_var(var, type, expr, stack, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
def refine_head_var({_, meta, _}, type, expr, stack, context) do
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_head_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
case context.vars do
%{^version => %{errored: true}} ->
{:ok, error_type(), context}
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
new_type = intersection(type, old_type)
@@ -95,26 +148,14 @@ defmodule Module.Types.Of do
off_traces: new_trace(expr, type, stack, off_traces)
}
context = %{context | vars: %{vars | version => data}}
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error, error_type(),
error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
data = Map.put(%{data | type: error_type()}, :errored, true)
context = %{context | vars: %{vars | version => data}}
{:error, old_type, context}
else
context = %{context | vars: %{vars | version => data}}
{:ok, new_type, context}
end
%{} = vars ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: new_trace(expr, type, stack, [])
}
context = %{context | vars: Map.put(vars, version, data)}
{:ok, type, context}
end
end
@@ -124,11 +165,57 @@ defmodule Module.Types.Of do
defp new_trace(expr, type, stack, traces),
do: [{expr, stack.file, type} | traces]
@doc """
Preserves `context` in first argument while
resetting it to the vars in the second argument.
"""
def reset_vars(context, %{vars: vars, conditional_vars: conditional_vars}),
do: %{context | vars: vars, conditional_vars: conditional_vars}
@doc """
Executes the args with acc using conditional variables.
"""
def with_conditional_vars(args, acc, expr, stack, context, fun) do
%{vars: vars, conditional_vars: conditional_vars} = context
{vars_conds, {acc, context}} =
Enum.map_reduce(args, {acc, context}, fn arg, {acc, context} ->
{acc, context} = fun.(arg, acc, %{context | vars: vars, conditional_vars: %{}})
%{vars: vars, conditional_vars: cond_vars} = context
{{vars, cond_vars}, {acc, context}}
end)
context = %{context | vars: vars, conditional_vars: conditional_vars}
{acc, reduce_conditional_vars(vars_conds, expr, stack, context)}
end
@doc """
Reduces conditional variables collected separately.
"""
def reduce_conditional_vars([{vars, cond} | vars_conds], expr, stack, context) do
Enum.reduce(Map.keys(cond), context, fn version, context ->
if Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
%{^version => %{type: type}} = vars
type =
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
%{^version => %{type: type}} = vars
union(acc, type)
end)
{_, context} = refine_body_var(version, type, expr, stack, context)
context
else
context
end
end)
end
## Implementations
impls = [
{Atom, atom()},
{BitString, binary()},
{BitString, bitstring()},
{Float, float()},
{Function, fun()},
{Integer, integer()},
@@ -141,14 +228,25 @@ defmodule Module.Types.Of do
{Any, term()}
]
@doc """
Currently, for protocol implementations, we only store
the open struct definition. This is because we don't want
to reconsolidate whenever the struct changes, but at the
moment we can't store references either. Ideally struct
types on protocol dispatches would be lazily resolved.
"""
def impl(for, mode \\ :closed)
for {for, type} <- impls do
def impl(unquote(for)), do: unquote(Macro.escape(type))
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
end
def impl(struct) do
# Elixir did not strictly require the implementation to be available, so we need a fallback.
def impl(struct, mode) do
# Elixir did not strictly require the implementation to be available,
# so we need to deal with such cases accordingly.
# TODO: Assume implementation is available on Elixir v2.0.
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
# A warning is emitted since v1.19+.
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info)
else
open_map(__struct__: atom([struct]))
@@ -161,7 +259,7 @@ defmodule Module.Types.Of do
Handles fetching a map key.
"""
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
case map_fetch_key(type, field) do
{_optional?, value_type} ->
{value_type, context}
@@ -176,107 +274,134 @@ defmodule Module.Types.Of do
def closed_map(pairs, expected, stack, context, of_fun) do
{pairs_types, context} = pairs(pairs, expected, stack, context, of_fun)
map =
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
# TODO: Use the fallback type to actually indicate if open or closed.
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
{dynamic?, domain, single, multiple} =
Enum.reduce(pairs_types, {false, [], [], []}, fn
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
dynamic? = dynamic? or dynamic_pair?
case pos_neg_domain do
# If atom is included in domain keys, it unions all previous
# single and multiple, except the ones negated:
#
# %{foo: :bar, term() => :baz}
# #=> %{foo: :bar or :baz, term() => :baz}
#
# %{foo: :bar, not :foo => :baz}
# #=> %{foo: :bar, term() => :baz}
#
# In case the negated term does not appear, we set it to none():
#
# %{foo: :bar, term() => :baz}
# #=> %{term() => :baz, foo: :bar or :baz}
#
# %{not :foo => :baz}
# #=> %{term() => :baz, foo: none()}
#
# In case we are dealing with multiple keys, we always merge the
# domain. A more precise approach would be to postpone doing so
# until the cartesian map is distributed but those should be very
# uncommon.
{[], negs, domain_keys} ->
if :atom in domain_keys do
{single, multiple} = union_negated(negs, value_type, single, multiple)
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
else
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
end
{pos, [], domain_keys} ->
domain =
case domain_keys do
[] -> domain
_ -> [{domain_keys, value_type} | domain]
end
case pos do
# Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples.
[key] when multiple == [] ->
{dynamic?, domain, [{key, value_type} | single], multiple}
_ ->
{dynamic?, domain, single, [{pos, value_type} | multiple]}
end
end
end)
{map, context}
non_multiple = Enum.reverse(single, domain)
map =
case Enum.reverse(multiple) do
[] ->
closed_map(non_multiple)
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
closed_map(non_multiple ++ [{key, type} | t])
end
|> Enum.reduce(&union/2)
end
{if(dynamic?, do: dynamic(map), else: map), context}
end
@doc """
Computes the types of key-value pairs.
"""
def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
Enum.map_reduce(pairs, context, fn {key, value}, context ->
{_key_type, context} = of_fun.(key, term(), stack, context)
{value_type, context} = of_fun.(value, term(), stack, context)
{{true, :none, value_type}, context}
end)
defp union_negated([], new_type, single, multiple) do
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
{single, multiple}
end
def pairs(pairs, expected, stack, context, of_fun) do
defp union_negated(negated, new_type, single, multiple) do
{single, matched} =
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
if key in negated do
{{key, old_type}, [key | matched]}
else
{{key, union(old_type, new_type)}, matched}
end
end)
multiple =
Enum.map(multiple, fn {keys, old_type} ->
{keys, union(old_type, new_type)}
end)
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
end
defp pairs(pairs, expected, stack, context, of_fun) do
Enum.map_reduce(pairs, context, fn {key, value}, context ->
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
expected_value_type =
with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
with {[key], [], []} <- pos_neg_domain,
{_, expected_value_type} <- map_fetch_key(expected, key) do
expected_value_type
else
_ -> term()
end
{value_type, context} = of_fun.(value, expected_value_type, stack, context)
{{dynamic_key? or gradual?(value_type), keys, value_type}, context}
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context}
end)
end
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{false, [key], context}
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{{[key], [], []}, false, context}
end
defp finite_key_type(key, stack, context, of_fun) do
defp map_key_type(key, stack, context, of_fun) do
{key_type, context} = of_fun.(key, term(), stack, context)
domain_keys = to_domain_keys(key_type)
case atom_fetch(key_type) do
{:finite, list} -> {gradual?(key_type), list, context}
_ -> {gradual?(key_type), :none, context}
end
end
@doc """
Builds permutation of maps according to the given pairs types.
"""
def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do
dynamic()
end
def permutate_map(pairs_types, _stack, of_map) do
{dynamic?, fallback, single, multiple, assert} =
Enum.reduce(pairs_types, {false, none(), [], [], []}, fn
{dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} ->
dynamic? = dynamic? or dynamic_pair?
case keys do
:none ->
fallback = union(fallback, value_type)
{fallback, assert} =
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
{union(fallback, type), [key | assert]}
end)
{fallback, assert} =
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
{union(fallback, type), keys ++ assert}
end)
{dynamic?, fallback, [], [], assert}
# Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples.
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert}
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert}
end
end)
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
end
|> Enum.reduce(&union/2)
pos_neg_domain =
case atom_fetch(key_type) do
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
{:infinite, list} -> {[], list, domain_keys}
:error -> {[], [], domain_keys}
end
if dynamic?, do: dynamic(map), else: map
{pos_neg_domain, gradual?(key_type), context}
end
defp cartesian_map(lists) do
@@ -293,7 +418,7 @@ defmodule Module.Types.Of do
Handles instantiation of a new struct.
"""
# TODO: Type check the fields match the struct
def struct_instance(struct, args, expected, meta, %{mode: mode} = stack, context, of_fun)
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
when is_atom(struct) do
{_info, context} = struct_info(struct, meta, stack, context)
@@ -301,10 +426,8 @@ defmodule Module.Types.Of do
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
with true <- mode != :traversal,
{_, expected_value_type} <- map_fetch(expected, key) do
expected_value_type
else
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
@@ -351,45 +474,66 @@ defmodule Module.Types.Of do
closed_map(pairs)
end
## Binary
## Bitstrings
@doc """
Handles binaries.
Handles bitstrings.
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _kind, _stack, context) do
context
def bitstring([], _kind, _stack, context) do
{binary(), context}
end
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
def bitstring([head], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
{alignment_to_type(alignment), context}
end
def binary([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
def bitstring([head | tail], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context)
bitstring_tail(tail, alignment, kind, stack, context)
end
defp binary_many([last], kind, stack, context) do
binary_segment(last, kind, [@prefix, last], stack, context)
defp bitstring_tail([last], alignment, kind, stack, context) do
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context)
{alignment_to_type(alignment(seg_alignment, alignment)), context}
end
defp binary_many([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
defp bitstring_tail([head | tail], alignment, kind, stack, context) do
{seg_alignment, context} =
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
end
defp alignment(left, right) when is_integer(left) and is_integer(right), do: left + right
defp alignment(_left, _right), do: :unknown
defp alignment_to_type(:unknown), do: bitstring()
defp alignment_to_type(integer) when rem(integer, 8) == 0, do: binary()
defp alignment_to_type(_integer), do: bitstring_no_binary()
# If the segment is a literal, the compiler has already checked its validity,
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
# so we just check the size.
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context)
when is_binary(left) or is_number(left) do
context
{_type, alignment_type} = specifier_type(kind, right)
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
# We don't need to check for bitstrings because the left side
# is either a binary (aligned), float (aligned), or integer
# (which we check below).
if alignment_type == :integer and alignment_value != :default do
{alignment_value, context}
else
{0, context}
end
end
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
{type, alignment_type} = specifier_type(kind, right)
expr = {:<<>>, meta, args}
{actual, context} =
@@ -406,10 +550,26 @@ defmodule Module.Types.Of do
end
if compatible?(actual, type) do
specifier_size(kind, right, stack, context)
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
case alignment_type do
:aligned ->
{0, context}
:integer when alignment_value == :default ->
{0, context}
# There is no size, so the aligment depends on the type.
# If the type is exclusively a binary, then it is aligned.
:bitstring when alignment_value == :default ->
if bitstring_no_binary_type?(actual), do: {:unknown, context}, else: {0, context}
_ ->
{alignment_value, context}
end
else
error = {:badbinary, kind, meta, expr, type, actual, context}
error(error, meta, stack, context)
{:unknown, error(error, meta, stack, context)}
end
end
@@ -425,39 +585,48 @@ defmodule Module.Types.Of do
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
defp specifier_type(:match, {:utf8, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned}
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned}
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer}
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring}
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring}
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned}
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned}
defp specifier_type(_kind, _specifier), do: {@integer, :integer}
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context})
when is_integer(arg) do
size = if unit == :default, do: arg, else: arg * unit
{size, context}
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, {_, context}) do
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
compatible_size(actual, expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)}
end
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
compatible_size(actual, expr, stack, context)
defp specifier_size(match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
{actual, context} = Module.Types.Pattern.of_size(match_or_guard, arg, expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)}
end
defp specifier_size(_kind, _specifier, _stack, context) do
context
# We currently assume the unit always comes before size
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do
{unit, context}
end
defp specifier_size(_kind, _specifier, _stack, align_context) do
align_context
end
defp compatible_size(actual, expr, stack, context) do
@@ -478,9 +647,12 @@ defmodule Module.Types.Of do
"""
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{_, mods} ->
{:finite, mods} ->
{mods, context}
{:infinite, _} ->
{[], context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)}
@@ -493,23 +665,6 @@ defmodule Module.Types.Of do
error(__MODULE__, warning, meta, stack, context)
end
def format_diagnostic({:refine_head_var, old_type, new_type, var, context}) do
traces = collect_traces(var, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(traces)
])
}
end
def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do
type = if kind == :match, do: "matching", else: "construction"
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Module.Types.Traverse do
@moduledoc false
# Traverses expressions to find local calls when inference is disabled.
# Literals
def of_expr(literal, _stack, context)
when is_atom(literal) or is_integer(literal) or is_float(literal) or is_binary(literal) or
is_pid(literal) or literal == [] do
context
end
# [expr, ...]
def of_expr(list, stack, context) when is_list(list) do
Enum.reduce(list, context, &of_expr(&1, stack, &2))
end
# {left, right}
def of_expr({left, right}, stack, context) do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
# <<...>>
def of_expr({:<<>>, _meta, args}, stack, context) do
Enum.reduce(args, context, fn
{:"::", _meta, [left, _right]}, context ->
of_expr(left, stack, context)
expr, context ->
of_expr(expr, stack, context)
end)
end
# Structs, map update, tail operator
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:%, :|] do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
# Tuples, maps
def of_expr({container, _meta, exprs}, stack, context) when container in [:{}, :%{}] do
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
end
# left = right, left <_ right
def of_expr({op, _meta, [_left, right]}, stack, context) when op in [:=, :<-] do
# Skip the left side (pattern), only traverse right
of_expr(right, stack, context)
end
# Blocks
def of_expr({:__block__, _, args}, stack, context) do
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# cond do ... end
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
Enum.reduce(clauses, context, fn {:->, _meta, [[head], body]}, context ->
context = of_expr(head, stack, context)
of_expr(body, stack, context)
end)
end
# Treat -> as patterns for simplicity
def of_expr({:->, _, [_head, body]}, stack, context) do
of_expr(body, stack, context)
end
# case expr do ... end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
context = of_expr(case_expr, stack, context)
of_expr(clauses, stack, context)
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses}, stack, context) do
of_expr(clauses, stack, context)
end
# try do ... end
def of_expr({:try, _meta, [blocks]}, stack, context) do
Enum.reduce(blocks, context, fn {_, clauses_or_body}, context ->
of_expr(clauses_or_body, stack, context)
end)
end
# receive do ... end
def of_expr({:receive, _meta, [blocks]}, stack, context) do
Enum.reduce(blocks, context, fn
{:do, clauses_or_empty_body}, context ->
of_expr(clauses_or_empty_body, stack, context)
{:after, [{:->, _meta, [[timeout], body]}]}, context ->
context = of_expr(timeout, stack, context)
of_expr(body, stack, context)
end)
end
# for, with
def of_expr({op, _meta, [_ | _] = args}, stack, context) when op in [:for, :with] do
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# fun.(args)
def of_expr({{:., _meta, [fun]}, _call_meta, args}, stack, context) do
context = of_expr(fun, stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# remote.fun(args)
def of_expr({{:., _, [remote, name]}, _meta, args}, stack, context)
when is_atom(name) do
context = of_expr(remote, stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# &Mod.fun/arity
def of_expr({:&, _, [{:/, _, [{{:., _, [_remote, name]}, _, []}, arity]}]}, _stack, context)
when is_atom(name) and is_integer(arity) do
context
end
# &fun/arity
def of_expr({:&, meta, [{:/, _, [{name, _, _ctx}, arity]}]}, stack, context)
when is_atom(name) and is_integer(arity) do
local_fun(meta, name, arity, stack, context)
end
# super(args)
def of_expr({:super, meta, args}, stack, context) when is_list(args) do
{_kind, name} = Keyword.fetch!(meta, :super)
context = local_fun(meta, name, length(args), stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# local_fun(args)
def of_expr({name, meta, args}, stack, context)
when is_atom(name) and is_list(args) do
context = local_fun(meta, name, length(args), stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# var
def of_expr({name, _meta, ctx}, _stack, context)
when is_atom(name) and is_atom(ctx) do
context
end
defp local_fun(meta, fun, arity, stack, context) do
case stack.local_handler.(meta, {fun, arity}, stack, context) do
false -> context
{_kind, _info, context} -> context
end
end
end
+81 -10
View File
@@ -129,6 +129,7 @@ defmodule OptionParser do
* `:integer` - parses the value as an integer
* `:float` - parses the value as a float
* `:string` - parses the value as a string
* `:regex` - parses the value as a regular expression with Unicode support
If a switch can't be parsed according to the given type, it is
returned in the invalid options list.
@@ -282,11 +283,11 @@ defmodule OptionParser do
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
** (OptionParser.ParseError) 1 error found!
--limit : Expected type integer, got "xyz"
--limit : Expected type integer, got "xyz"...
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
** (OptionParser.ParseError) 1 error found!
--unknown : Unknown option
--unknown : Unknown option...
iex> OptionParser.parse!(
...> ["-l", "xyz", "-f", "bar"],
@@ -295,7 +296,7 @@ defmodule OptionParser do
...> )
** (OptionParser.ParseError) 2 errors found!
-l : Expected type integer, got "xyz"
-f : Expected type integer, got "bar"
-f : Expected type integer, got "bar"...
"""
@spec parse!(argv, options) :: {parsed, argv}
@@ -354,7 +355,7 @@ defmodule OptionParser do
...> strict: [number: :integer]
...> )
** (OptionParser.ParseError) 1 error found!
--number : Expected type integer, got "lib"
--number : Expected type integer, got "lib"...
iex> OptionParser.parse_head!(
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
@@ -362,7 +363,7 @@ defmodule OptionParser do
...> )
** (OptionParser.ParseError) 2 errors found!
--verbose : Missing argument of type integer
--source : Expected type integer, got "lib"
--source : Expected type integer, got "lib"...
"""
@spec parse_head!(argv, options) :: {parsed, argv}
@@ -664,7 +665,7 @@ defmodule OptionParser do
end
defp validate_switch({_name, type_or_type_and_modifiers}) do
valid = [:boolean, :count, :integer, :float, :string, :keep]
valid = [:boolean, :count, :integer, :float, :string, :regex, :keep]
invalid = List.wrap(type_or_type_and_modifiers) -- valid
if invalid != [] do
@@ -704,6 +705,12 @@ defmodule OptionParser do
_ -> {true, value}
end
:regex in kinds ->
case Regex.compile(value, "u") do
{:ok, regex} -> {false, regex}
{:error, _} -> {true, value}
end
true ->
{false, value}
end
@@ -863,15 +870,20 @@ defmodule OptionParser do
error_count = length(errors)
error = if error_count == 1, do: "error", else: "errors"
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
slogan =
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
case format_available_options(opts, types) do
"" -> slogan
available_options -> slogan <> "\n\n#{available_options}"
end
end
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
@@ -891,7 +903,13 @@ defmodule OptionParser do
defp format_error({option, value}, opts, types) do
type = get_type(option, opts, types)
"#{option} : Expected type #{type}, got #{inspect(value)}"
with :regex <- type,
{:error, {reason, position}} <- Regex.compile(value, "u") do
"#{option} : Invalid regular expression #{inspect(value)}: #{reason} at position #{position}"
else
_ -> "#{option} : Expected type #{type}, got #{inspect(value)}"
end
end
defp get_type(option, opts, types) do
@@ -917,4 +935,57 @@ defmodule OptionParser do
option = String.replace(source, "_", "-")
if score < current, do: best, else: {option, score}
end
defp format_available_options(opts, switches) do
reverse_aliases =
opts
|> Keyword.get(:aliases, [])
|> Enum.reduce(%{}, fn {alias, target}, acc ->
Map.update(acc, target, [alias], &[alias | &1])
end)
formatted_options =
switches
|> Enum.sort()
|> Enum.map(fn {name, types} ->
types = List.wrap(types)
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
base = add_aliases(base, name, reverse_aliases)
if :keep in types do
base <> " (may be given more than once)"
else
base
end
end
end)
if formatted_options == [] do
""
else
"Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1))
end
end
defp add_aliases(base, name, reverse_aliases) do
case Map.get(reverse_aliases, name, []) do
[] ->
base
alias_list ->
alias_str =
alias_list
|> Enum.sort()
|> Enum.map_join(", ", &("-" <> Atom.to_string(&1)))
base <> " (alias: #{alias_str})"
end
end
end
+2 -2
View File
@@ -450,7 +450,7 @@ defmodule PartitionSupervisor do
end
@doc """
Returns a list with information about all children.
Returns a list with information about all children of the given supervisor.
This function returns a list of tuples containing:
@@ -546,7 +546,7 @@ defmodule PartitionSupervisor do
end
@doc false
def unregister_name(_, _) do
def unregister_name(_) do
raise "{:via, PartitionSupervisor, _} cannot be given on unregistration"
end
end
+5 -3
View File
@@ -22,6 +22,8 @@ defmodule Path do
"""
@type t :: IO.chardata()
@type relative_to_opts :: [force: boolean()]
@doc """
Converts the given path to an absolute one.
@@ -401,7 +403,7 @@ defmodule Path do
Path.relative_to("../foo", "/usr/local") #=> "../foo"
"""
@spec relative_to(t, t, keyword) :: binary
@spec relative_to(t, t, relative_to_opts) :: binary
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
os_type = major_os_type()
split_path = split(path)
@@ -479,7 +481,7 @@ defmodule Path do
Check `relative_to/3` for the supported options.
"""
@spec relative_to_cwd(t, keyword) :: binary
@spec relative_to_cwd(t, relative_to_opts) :: binary
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
@@ -801,7 +803,7 @@ defmodule Path do
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
"""
@spec wildcard(t, keyword) :: [binary]
@spec wildcard(t, match_dot: boolean()) :: [binary]
def wildcard(glob, opts \\ []) when is_list(opts) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
+1 -1
View File
@@ -78,7 +78,7 @@ defmodule Port do
The port can be opened through four main mechanisms.
As a short summary, prefer to using the `:spawn` and `:spawn_executable`
As a short summary, prefer to use the `:spawn` and `:spawn_executable`
options mentioned below. The other two options, `:spawn_driver` and `:fd`
are for advanced usage within the VM. Also consider using `System.cmd/3`
if all you want is to execute a program and retrieve its return value.
+16 -17
View File
@@ -202,24 +202,23 @@ defmodule Process do
@doc """
Sends an exit signal with the given `reason` to `pid`.
The following behavior applies if `reason` is any term except `:normal`
or `:kill`:
Exit behavior differs based on the value of `reason`:
1. If `pid` is not trapping exits, `pid` will exit with the given
`reason`.
- If `:normal`, `pid` will not exit unless it is the calling process, in
which case it will exit with the reason `:normal`. If it is trapping exits,
the exit signal is transformed into a message `{:EXIT, from, :normal}` and
delivered to its message queue.
2. If `pid` is trapping exits, the exit signal is transformed into a
message `{:EXIT, from, reason}` and delivered to the message queue
of `pid`.
- If `:kill`, which occurs when `Process.exit(pid, :kill)` is called, an
untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
the calling process, in which case it will exit with the reason `:normal`).
If it is trapping exits, the exit signal is transformed into a message
`{:EXIT, from, :normal}` and delivered to its message queue.
- If any other term and `pid` is not trapping exits, `pid` will exit with
the given `reason`.
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
an untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
- If any other term and `pid` is trapping exits, the exit signal is
transformed into a message `{:EXIT, from, reason}` and delivered to its
message queue.
Inlined by the compiler.
@@ -535,7 +534,7 @@ defmodule Process do
If the process is already dead when calling `Process.monitor/1`, a
`:DOWN` message is delivered immediately.
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
See ["Links and monitors"](genservers.md#links-and-monitors)
for an example. See `:erlang.monitor/2` for more information.
Inlined by the compiler.
@@ -840,7 +839,7 @@ defmodule Process do
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:priority, priority_level) :: priority_level
@spec flag(:save_calls, 0..10000) :: 0..10000
@spec flag(:save_calls, 0..10_000) :: 0..10_000
@spec flag(:sensitive, boolean) :: boolean
@spec flag(:trap_exit, boolean) :: boolean
defdelegate flag(flag, value), to: :erlang, as: :process_flag
@@ -859,7 +858,7 @@ defmodule Process do
Inlined by the compiler.
"""
@spec flag(pid, :save_calls, 0..10000) :: 0..10000
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
@doc """
+91 -54
View File
@@ -21,7 +21,7 @@ defmodule Protocol do
the data structure.
Although Elixir includes specific functions such as `tuple_size`,
`binary_size` and `map_size`, sometimes we want to be able to
`byte_size` and `map_size`, sometimes we want to be able to
retrieve the size of a data structure regardless of its type.
In Elixir we can write polymorphic code, i.e. code that works
with different shapes/types, by using protocols. A size protocol
@@ -267,6 +267,8 @@ defmodule Protocol do
@optional_callbacks __deriving__: 2
@elixir_checker_version :elixir_erl.checker_version()
@doc false
defmacro def(signature)
@@ -451,7 +453,7 @@ defmodule Protocol do
true
"""
@spec extract_protocols([charlist | String.t()]) :: [atom]
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_protocols(paths) do
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
case attributes[:__protocol__] do
@@ -480,7 +482,7 @@ defmodule Protocol do
true
"""
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_impls(protocol, paths) when is_atom(protocol) do
prefix = Atom.to_charlist(protocol) ++ [?.]
@@ -494,17 +496,25 @@ defmodule Protocol do
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
# Do not use protocols as they may be consolidating
path = if(is_list(path), do: path, else: String.to_charlist(path)),
file <- list_dir(path),
{path, files} = list_dir(path),
file <- files,
mod = extract_from_file(path, file, prefix, callback),
do: mod
end
# Do not use protocols as they may be consolidating
defp list_dir({path, files}) when is_list(path) and is_list(files) do
{path, files}
end
defp list_dir(path) when is_binary(path) do
list_dir(String.to_charlist(path))
end
defp list_dir(path) when is_list(path) do
case :file.list_dir(path) do
{:ok, files} -> files
_ -> []
{:ok, files} -> {path, files}
_ -> {path, []}
end
end
@@ -564,31 +574,34 @@ defmodule Protocol do
# Ensure the types are sorted so the compiled beam is deterministic
types = Enum.sort(types)
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
{:ok, definitions, signatures} <-
consolidate(protocol, any, definitions, signatures, types),
do: compile(definitions, signatures, compile_info)
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
{:ok, definitions, checker} <-
consolidate(protocol, any, definitions, checker, types),
do: compile(definitions, checker, compile_info)
end
defp beam_protocol(protocol) do
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
opts = [:allow_missing_chunks]
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
%{attributes: attributes, definitions: definitions} = module_map
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
# TODO: Remove this on Elixir v1.23.
signatures = Map.get(module_map, :signatures, %{})
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
case attributes[:__protocol__] do
[fallback_to_any: any] ->
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
checker =
with true <- is_binary(checker),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
contents
else
_ -> nil
end
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
{:ok, any, definitions, checker, {module_map, specs, chunks}}
_ ->
{:error, :not_a_protocol}
@@ -607,7 +620,7 @@ defmodule Protocol do
end
# Consolidate the protocol for faster implementations and fine-grained type information.
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
case List.keytake(definitions, {:__protocol__, 1}, 0) do
{protocol_def, definitions} ->
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
@@ -623,25 +636,37 @@ defmodule Protocol do
protocol_def = change_protocol(protocol_def, types)
impl_for = change_impl_for(impl_for, protocol, types)
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
signatures = Enum.into(new_signatures, signatures)
{:ok, definitions, signatures}
checker =
if checker do
update_in(checker.exports, fn exports ->
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
for {fun, info} <- exports do
if sig = Map.get(signatures, fun) do
{fun, %{info | sig: sig}}
else
{fun, info}
end
end
end)
end
{:ok, definitions, checker}
nil ->
{:error, :not_a_protocol}
end
end
defp new_signatures(definitions, protocol_funs, protocol, types) do
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
alias Module.Types.Descr
types_minus_any = List.delete(types, Any)
clauses =
types
|> List.delete(Any)
|> Enum.map(fn impl ->
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
Enum.map(types_minus_any, fn impl ->
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
end)
{domain, impl_for, impl_for!} =
@@ -656,10 +681,16 @@ defmodule Protocol do
end
_ ->
structs_domain =
case structs do
[] -> Descr.none()
_ -> Descr.open_map(__struct__: Descr.atom(structs))
end
domain =
clauses
|> Enum.map(fn {[domain], _} -> domain end)
|> Enum.reduce(&Descr.union/2)
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
Descr.union(Module.Types.Of.impl(impl, :open), acc)
end)
not_domain = Descr.negation(domain)
@@ -680,10 +711,12 @@ defmodule Protocol do
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
end
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
Map.new(
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
)
end
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
@@ -752,11 +785,9 @@ defmodule Protocol do
end
# Finally compile the module and emit its bytecode.
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
# TODO: Remove this on Elixir v1.23.
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
module_map = %{module_map | definitions: definitions}
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
end
## Definition callbacks
@@ -769,16 +800,7 @@ defmodule Protocol do
@before_compile Protocol
# We don't allow function definition inside protocols
import Kernel,
except: [
def: 1,
def: 2,
defdelegate: 2,
defguard: 1,
defguardp: 1,
defstruct: 1,
defexception: 1
]
import Kernel, except: [def: 1, def: 2]
# Import the new `def` that is used by protocols
import Protocol, only: [def: 1]
@@ -841,6 +863,21 @@ defmodule Protocol do
)
end
extra =
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
functions) --
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
# TODO: Make an error on Elixir v2.0
if extra != [] do
warn(
"protocols can only define functions without implementation via def/1, found: " <>
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
env,
nil
)
end
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
+6 -9
View File
@@ -207,12 +207,6 @@ defmodule Range do
%Range{first: first, last: last, step: step}
end
def new(first, last) do
raise ArgumentError,
"ranges (first..last) expect both sides to be integers, " <>
"got: #{inspect(first)}..#{inspect(last)}"
end
@doc """
Creates a new range with `step`.
@@ -290,9 +284,12 @@ defmodule Range do
"""
@doc since: "1.14.0"
@spec shift(t, integer) :: t
def shift(first..last//step, steps_to_shift)
when is_integer(steps_to_shift) do
new(first + steps_to_shift * step, last + steps_to_shift * step, step)
def shift(%Range{} = range, 0), do: range
def shift(first..last//step, steps_to_shift) when is_integer(steps_to_shift) do
shift = steps_to_shift * step
new(first + shift, last + shift, step)
end
@doc """
+12 -4
View File
@@ -27,8 +27,9 @@ defmodule Record do
## Types
Types can be defined for tuples with the `record/2` macro (only available in
typespecs). This macro will expand to a tuple as seen in the example below:
Types can be defined for tuples with the `record/2` construct (which is only
available in typespecs), with the record name as an atom and a keyword list
of fields and their types as argument:
defmodule MyModule do
require Record
@@ -45,6 +46,13 @@ defmodule Record do
a module by calling `Code.fetch_docs/1`.
"""
@type extract_opts :: [
from: binary(),
from_lib: binary(),
includes: [binary()],
macros: keyword()
]
@doc """
Extracts record information from an Erlang file.
@@ -102,7 +110,7 @@ defmodule Record do
]
"""
@spec extract(name :: atom, keyword) :: keyword
@spec extract(name :: atom, extract_opts) :: keyword
def extract(name, opts) when is_atom(name) and is_list(opts) do
Record.Extractor.extract(name, opts)
end
@@ -119,7 +127,7 @@ defmodule Record do
Accepts the same options as listed for `Record.extract/2`.
"""
@spec extract_all(keyword) :: [{name :: atom, keyword}]
@spec extract_all(extract_opts) :: [{name :: atom, keyword}]
def extract_all(opts) when is_list(opts) do
Record.Extractor.extract_all(opts)
end
+159 -14
View File
@@ -3,6 +3,8 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Regex do
# TODO: Remove the "Starting from Erlang/OTP 28" part in the Modifiers'
# section once Erlang/OTP 28+ is exclusively supported.
@moduledoc ~S"""
Provides regular expressions for Elixir.
@@ -76,9 +78,16 @@ defmodule Regex do
* `:caseless` (i) - adds case insensitivity
* `:dotall` (s) - causes dot to match newlines and also set newline to
anycrlf; the new line setting can be overridden by setting `(*CR)` or
`(*LF)` or `(*CRLF)` or `(*ANY)` according to `:re` documentation
* `:dotall` (s) - causes dot to match newlines and also sets newline to
`(*ANYCRLF)`.\
The new line setting, as described in the [`:re` documentation](`:re`),
can be overridden by starting the regular expression pattern with:
* `(*CR)` - carriage return
* `(*LF)` - line feed
* `(*CRLF)` - carriage return, followed by line feed
* `(*ANYCRLF)` - any of the three above
* `(*ANY)` - all Unicode newline sequences
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
each line; use `\A` and `\z` to match the end or beginning of the string
@@ -92,6 +101,13 @@ defmodule Regex do
* `:ungreedy` (U) - inverts the "greediness" of the regexp
(the previous `r` option is deprecated in favor of `U`)
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
which can be shared across nodes or passed through config, at the cost of a runtime
overhead to re-import it every time it is executed.
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
and do not need to be exported in order to be shared in these versions.
## Captures
Many functions in this module handle what to capture in a regex
@@ -164,6 +180,11 @@ defmodule Regex do
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: [term]}
@type named_captures_opts :: [
return: :binary | :index,
offset: non_neg_integer()
]
defmodule CompileError do
@moduledoc """
An exception raised when a regular expression could not be compiled.
@@ -237,7 +258,7 @@ defmodule Regex do
This checks the version stored in the regular expression
and recompiles the regex in case of version mismatch.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile(%Regex{} = regex) do
@@ -247,17 +268,46 @@ defmodule Regex do
@doc """
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile!(regex) do
regex
end
@doc """
Imports a `regex` that has been exported, otherwise returns the `regex` unchanged.
This means it will lose the ability to be sent across nodes or passed through config,
but will be faster since it won't need to be imported on the fly every time it is executed.
Exported regexes only exist on OTP 28, so this has no effect on older versions.
## Examples
Regex.import(~r/foo/E)
~r/foo/
Regex.import(~r/foo/)
~r/foo/
"""
@doc since: "1.20.0"
@spec import(t) :: t
def import(%Regex{re_pattern: re_pattern} = regex) do
case re_pattern do
{:re_exported_pattern, _, _, _, _} ->
%{regex | re_pattern: :re.import(re_pattern), opts: regex.opts -- [:export]}
_ ->
regex
end
end
@doc """
Returns the version of the underlying Regex engine.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "Use :re.version() instead"
@doc since: "1.4.0"
def version do
@@ -301,7 +351,7 @@ defmodule Regex do
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
to see the possible capture values.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to zero.
Defaults to `0`.
## Examples
@@ -321,7 +371,7 @@ defmodule Regex do
["d", ""]
"""
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
@spec run(t, binary, capture_opts) :: nil | [binary] | [{integer, integer}]
def run(regex, string, options \\ [])
def run(%Regex{} = regex, string, options) when is_binary(string) do
@@ -343,6 +393,8 @@ defmodule Regex do
* `:return` - when set to `:index`, returns byte index and match length.
Defaults to `:binary`.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to `0`.
## Examples
@@ -363,7 +415,7 @@ defmodule Regex do
You can then use `binary_part/3` to fetch the relevant part from the given string.
"""
@spec named_captures(t, String.t(), keyword) :: map | nil
@spec named_captures(t, String.t(), named_captures_opts) :: map | nil
def named_captures(regex, string, options \\ []) when is_binary(string) do
names = names(regex)
options = Keyword.put(options, :capture, names)
@@ -499,7 +551,7 @@ defmodule Regex do
"""
@spec names(t) :: [String.t()]
def names(%Regex{re_pattern: re_pattern}) do
{:namelist, names} = :re.inspect(re_pattern, :namelist)
{:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist)
names
end
@@ -516,7 +568,7 @@ defmodule Regex do
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
to see the possible capture values.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to zero.
Defaults to `0`.
## Examples
@@ -545,7 +597,7 @@ defmodule Regex do
[["cd"], ["ce"]]
"""
@spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
@spec scan(t(), String.t(), capture_opts) :: [[String.t()]] | [[{integer(), integer()}]]
def scan(regex, string, options \\ [])
def scan(%Regex{} = regex, string, options) when is_binary(string) do
@@ -569,10 +621,36 @@ defmodule Regex do
%Regex{source: source, opts: compile_opts} = regex
:re.run(string, source, compile_opts ++ options)
else
_ -> :re.run(string, re_pattern, options)
_ -> :re.run(string, maybe_import_pattern(re_pattern), options)
end
end
@compile {:inline, maybe_import_pattern: 1}
@compile {:no_warn_undefined, {:re, :import, 1}}
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
do: :re.import(exported)
defp maybe_import_pattern(pattern), do: pattern
@typedoc """
Options for regex functions that capture matches.
"""
@type capture_opts :: [
return: :binary | :index,
capture: :all | :first | :all_but_first | :none | :all_names | [binary() | atom()],
offset: non_neg_integer()
]
@typedoc """
Options for `split/3`.
"""
@type split_opts :: [
parts: pos_integer() | :infinity,
trim: boolean(),
on: :first | :all | :all_but_first | :none | :all_names | [atom() | integer()],
include_captures: boolean()
]
@doc """
Splits the given target based on the given pattern and in the given number of
parts.
@@ -626,7 +704,7 @@ defmodule Regex do
["a", "b", "c"]
"""
@spec split(t, String.t(), [term]) :: [String.t()]
@spec split(t, String.t(), split_opts) :: [String.t()]
def split(regex, string, options \\ [])
def split(%Regex{}, "", opts) do
@@ -972,6 +1050,73 @@ defmodule Regex do
translate_options(t, [:ungreedy | acc])
end
defp translate_options(<<?E, t::binary>>, acc) do
# on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed
# (regexes aren't using references and can be shared across nodes or stored in config)
# TODO: remove this check on Erlang/OTP 28+ and update docs
case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
true -> translate_options(t, [:export | acc])
false -> translate_options(t, acc)
end
end
defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}
@doc false
def __escape__(%{__struct__: Regex} = regex) do
# OTP 28.0 introduced refs in patterns, which can't be used in AST anymore
# OTP 28.1 introduced :re.import/1 which allows us to work with pre-compiled binaries again
pattern_ast =
cond do
# TODO: Remove this when we require Erlang/OTP 28+
# Before OTP 28.0, patterns did not contain any refs and could be safely be escaped
:erlang.system_info(:otp_release) < [?2, ?8] ->
Macro.escape(regex.re_pattern)
:lists.member(:export, regex.opts) ->
Macro.escape(regex.re_pattern)
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
quote do
Regex.__import_pattern__(unquote(Macro.escape(exported)))
end
# we now that the Regex module is defined at this stage, so this macro can be safely called
|> Macro.update_meta(&([required: true] ++ &1))
# TODO: Remove this when we require Erlang/OTP 28.1+
# OTP 28.0 works in degraded mode performance-wise, we need to recompile from the source
true ->
quote do
{:ok, pattern} =
:re.compile(unquote(Macro.escape(regex.source)), unquote(Macro.escape(regex.opts)))
pattern
end
end
quote do
%{
__struct__: unquote(Regex),
re_pattern: unquote(pattern_ast),
source: unquote(Macro.escape(regex.source)),
opts: unquote(Macro.escape(regex.opts))
}
end
end
@doc false
defmacro __import_pattern__(pattern) do
if __CALLER__.context in [:match, :guard] do
raise ArgumentError, "escaped Regex structs are not allowed in match or guards"
end
quote do
:re.import(unquote(pattern))
end
end
end
+132 -36
View File
@@ -187,7 +187,7 @@ defmodule Registry do
Note that the registry uses one ETS table plus two ETS tables per partition.
"""
@keys [:unique, :duplicate]
@keys [:unique, :duplicate, {:duplicate, :key}, {:duplicate, :pid}]
@all_info -1
@key_info -2
@@ -195,7 +195,7 @@ defmodule Registry do
@type registry :: atom
@typedoc "The type of the registry"
@type keys :: :unique | :duplicate
@type keys :: :unique | :duplicate | {:duplicate, :key} | {:duplicate, :pid}
@typedoc "The type of keys allowed on registration"
@type key :: term
@@ -242,6 +242,11 @@ defmodule Registry do
{:register, registry, key, registry_partition :: pid, value}
| {:unregister, registry, key, registry_partition :: pid}
@typedoc """
Options used for `dispatch/4`.
"""
@type dispatch_opts :: [parallel: boolean()]
## Via callbacks
@doc false
@@ -261,8 +266,8 @@ defmodule Registry do
:undefined
end
{kind, _, _} ->
raise ArgumentError, ":via is not supported for #{kind} registries"
{{:duplicate, _}, _, _} ->
raise ArgumentError, ":via is not supported for duplicate registries"
end
end
@@ -324,11 +329,25 @@ defmodule Registry do
{Registry, keys: :unique, name: MyApp.Registry, partitions: System.schedulers_online()}
], strategy: :one_for_one)
For `:duplicate` registries with many different keys (e.g., many topics with
few subscribers each), you can optimize key-based lookups by partitioning by key:
Registry.start_link(
keys: {:duplicate, :key},
name: MyApp.TopicRegistry,
partitions: System.schedulers_online()
)
This allows key-based lookups to check only a single partition instead of
searching all partitions. Use the default `:pid` partitioning when you have
fewer keys with many entries each (e.g., one topic with many subscribers).
## Options
The registry requires the following keys:
* `:keys` - chooses if keys are `:unique` or `:duplicate`
* `:keys` - chooses if keys are `:unique`, `:duplicate`,
`{:duplicate, :key}`, or `{:duplicate, :pid}`
* `:name` - the name of the registry and its tables
The following keys are optional:
@@ -340,16 +359,40 @@ defmodule Registry do
crashes. Messages sent to listeners are of type `t:listener_message/0`.
* `:meta` - a keyword list of metadata to be attached to the registry.
For `:duplicate` registries, you can specify the partitioning strategy
directly in the `:keys` option:
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
when you have keys with many entries (e.g., one topic with many subscribers).
This is the traditional behavior and groups all entries from the same process together.
* `{: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.
"""
@doc since: "1.5.0"
@spec start_link([start_option]) :: {:ok, pid} | {:error, term}
def start_link(options) do
keys = Keyword.get(options, :keys)
if keys not in @keys do
raise ArgumentError,
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
end
# Validate and normalize keys format
kind =
case keys do
{:duplicate, partition_strategy} when partition_strategy in [:key, :pid] ->
{:duplicate, partition_strategy}
:unique ->
:unique
:duplicate ->
{:duplicate, :pid}
_ ->
raise ArgumentError,
"expected :keys to be given and be one of :unique, :duplicate, {:duplicate, :key}, or {:duplicate, :pid}, got: #{inspect(keys)}"
end
name =
case Keyword.fetch(options, :name) do
@@ -392,11 +435,18 @@ defmodule Registry do
# The @info format must be kept in sync with Registry.Partition optimization.
entries = [
{@all_info, {keys, partitions, nil, nil, listeners}},
{@key_info, {keys, partitions, nil}} | meta
{@all_info, {kind, partitions, nil, nil, listeners}},
{@key_info, {kind, partitions, nil}} | meta
]
Registry.Supervisor.start_link(keys, name, partitions, listeners, entries, compressed)
Registry.Supervisor.start_link(
kind,
name,
partitions,
listeners,
entries,
compressed
)
end
@doc false
@@ -463,7 +513,8 @@ defmodule Registry do
end
{kind, _, _} ->
raise ArgumentError, "Registry.update_value/3 is not supported for #{kind} registries"
raise ArgumentError,
"Registry.update_value/3 is not supported for #{inspect(kind)} registries"
end
end
@@ -483,9 +534,15 @@ defmodule Registry do
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`.
"""
@doc since: "1.4.0"
@spec dispatch(registry, key, dispatcher, keyword) :: :ok
@spec dispatch(registry, key, dispatcher, dispatch_opts) :: :ok
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [term()]}
def dispatch(registry, key, mfa_or_fun, opts \\ [])
when is_atom(registry) and is_function(mfa_or_fun, 1)
@@ -497,12 +554,12 @@ defmodule Registry do
|> List.wrap()
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
key_ets
|> safe_lookup_second(key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{:duplicate, partitions, _} ->
{{:duplicate, _}, partitions, _} ->
if Keyword.get(opts, :parallel, false) do
registry
|> dispatch_parallel(key, mfa_or_fun, partitions)
@@ -614,10 +671,14 @@ defmodule Registry do
[]
end
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
safe_lookup_second(key_ets, key)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
safe_lookup_second(key_ets!(registry, partition), key)
{{:duplicate, :pid}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- safe_lookup_second(key_ets!(registry, partition), key),
do: pair
@@ -738,10 +799,10 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
:ets.select(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, _}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- :ets.select(key_ets!(registry, partition), spec),
do: pair
@@ -784,15 +845,34 @@ defmodule Registry do
@spec keys(registry, pid) :: [key]
def keys(registry, pid) when is_atom(registry) and is_pid(pid) do
{kind, partitions, _, pid_ets, _} = info!(registry)
{_, pid_ets} = pid_ets || pid_ets!(registry, pid, partitions)
pid_etses =
if pid_ets do
{_, pid_ets} = pid_ets
[pid_ets]
else
case kind do
{:duplicate, :key} ->
for partition <- 0..(partitions - 1) do
{_, pid_ets} = pid_ets!(registry, partition)
pid_ets
end
_ ->
{_, pid_ets} = pid_ets!(registry, pid, partitions)
[pid_ets]
end
end
keys =
try do
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
Enum.flat_map(pid_etses, fn pid_ets ->
try do
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
end)
# Handle the possibility of fake keys
keys = gather_keys(keys, [], false)
@@ -871,8 +951,17 @@ defmodule Registry do
[]
end
{:duplicate, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, pid, partitions)
{{:duplicate, _}, 1, key_ets} ->
for {^pid, value} <- safe_lookup_second(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
{{: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
end
end
@@ -1110,7 +1199,7 @@ defmodule Registry do
end
end
defp register_key(:duplicate, key_ets, _key, entry) do
defp register_key({:duplicate, _}, key_ets, _key, entry) do
true = :ets.insert(key_ets, entry)
:ok
end
@@ -1328,10 +1417,10 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
:ets.select_count(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, _}, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
@@ -1501,7 +1590,12 @@ defmodule Registry do
{hash(key, partitions), hash(pid, partitions)}
end
defp partitions(:duplicate, _key, pid, partitions) do
defp partitions({:duplicate, :key}, key, _pid, partitions) do
partition = hash(key, partitions)
{partition, partition}
end
defp partitions({:duplicate, :pid}, _key, pid, partitions) do
partition = hash(pid, partitions)
{partition, partition}
end
@@ -1565,9 +1659,10 @@ defmodule Registry.Supervisor do
defp strategy_for_kind(:unique), do: :one_for_all
# Duplicate registries have both key and pid partitions hashed
# by pid. This means that, if a PID partition crashes, all of
# by key ({:duplicate, :key}) or pid ({:duplicate, :pid}).
# This means that, if a PID or key partition crashes, all of
# its associated entries are in its sibling table, so we crash one.
defp strategy_for_kind(:duplicate), do: :one_for_one
defp strategy_for_kind({:duplicate, _}), do: :one_for_one
end
defmodule Registry.Partition do
@@ -1622,6 +1717,7 @@ defmodule Registry.Partition do
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
Process.flag(:trap_exit, true)
key_ets = init_key_ets(kind, key_partition, compressed)
pid_ets = init_pid_ets(kind, pid_partition)
@@ -1648,7 +1744,7 @@ 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_partition, compressed) do
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
+22 -22
View File
@@ -964,17 +964,13 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:suspended, [val], next} ->
do_transform_user(val, user_acc, :cont, next, inner_acc, funs)
{:suspended, vals, next} ->
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
{_, result} ->
{_, vals} ->
# Do not attempt to call the resource again, it has either done or halted
next = fn _ -> {:done, []} end
case result do
[val] -> do_transform_user(val, user_acc, :last, next, inner_acc, funs)
[] -> do_transform(user_acc, :last, next, inner_acc, funs)
end
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
end
end
@@ -989,7 +985,7 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, :halt, next, inner_acc, funs)
result -> do_transform_result(result, [], :halt, next, inner_acc, funs)
end
else
do_transform(user_acc, :halt, next, inner_acc, funs)
@@ -1002,7 +998,11 @@ defmodule Stream do
{:halted, elem(inner_acc, 1)}
end
defp do_transform_user(val, user_acc, next_op, next, inner_acc, funs) do
defp do_transform_user([], user_acc, next_op, next, inner_acc, funs) do
do_transform(user_acc, next_op, next, inner_acc, funs)
end
defp do_transform_user([val | vals], user_acc, next_op, next, inner_acc, funs) do
{user, _, _, _, after_fun} = funs
try do
@@ -1013,20 +1013,20 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, next_op, next, inner_acc, funs)
result -> do_transform_result(result, vals, next_op, next, inner_acc, funs)
end
end
defp do_transform_result(result, next_op, next, inner_acc, funs) do
defp do_transform_result(result, vals, next_op, next, inner_acc, funs) do
{_, fun, inner, _, after_fun} = funs
case result do
{[], user_acc} ->
do_transform(user_acc, next_op, next, inner_acc, funs)
do_transform_user(vals, user_acc, next_op, next, inner_acc, funs)
{list, user_acc} when is_list(list) ->
reduce = &Enumerable.List.reduce(list, &1, fun)
do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs)
{:halt, user_acc} ->
next.({:halt, []})
@@ -1035,11 +1035,11 @@ defmodule Stream do
{other, user_acc} ->
reduce = &Enumerable.reduce(other, &1, inner)
do_transform_inner_enum(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_enum(vals, user_acc, next_op, next, inner_acc, reduce, funs)
end
end
defp do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs) do
defp do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1051,7 +1051,7 @@ defmodule Stream do
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:done, acc} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:halted, acc} ->
next.({:halt, []})
@@ -1059,12 +1059,12 @@ defmodule Stream do
{:halted, acc}
{:suspended, acc, continuation} ->
resume = &do_transform_inner_list(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_list(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
defp do_transform_inner_enum(user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
defp do_transform_inner_enum(vals, user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1078,7 +1078,7 @@ defmodule Stream do
# The user wanted to cont/suspend but the stream halted,
# so we continue with the user intention.
{:halted, [inner_op | acc]} when op != :halt and inner_op != :halt ->
do_transform(user_acc, next_op, next, {inner_op, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {inner_op, acc}, funs)
{:halted, [_ | acc]} ->
next.({:halt, []})
@@ -1086,10 +1086,10 @@ defmodule Stream do
{:halted, acc}
{:done, [_ | acc]} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:suspended, [_ | acc], continuation} ->
resume = &do_transform_inner_enum(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_enum(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
+14 -4
View File
@@ -22,7 +22,7 @@ defmodule String do
"hello world"
The functions in this module act according to
[The Unicode Standard, Version 16.0.0](http://www.unicode.org/versions/Unicode16.0.0/).
[The Unicode Standard, Version 17.0.0](https://www.unicode.org/versions/Unicode17.0.0/).
## Interpolation
@@ -298,6 +298,15 @@ defmodule String do
| [nonempty_binary]
| (compiled_search_pattern :: :binary.cp())
@type split_opts :: [
parts: pos_integer() | :infinity,
trim: boolean()
]
@type splitter_opts :: [trim: boolean()]
@type replace_opts :: [global: boolean()]
@conditional_mappings [:greek, :turkic]
@doc """
@@ -502,7 +511,8 @@ defmodule String do
["a", "b", " c "]
"""
@spec split(t, pattern | Regex.t(), keyword) :: [t]
@spec split(t, pattern, split_opts()) :: [t]
@spec split(t, Regex.t(), Regex.split_opts()) :: [t]
def split(string, pattern, options \\ [])
def split(string, %Regex{} = pattern, options) when is_binary(string) and is_list(options) do
@@ -607,7 +617,7 @@ defmodule String do
["1", "2", "3", "4"]
"""
@spec splitter(t, pattern, keyword) :: Enumerable.t()
@spec splitter(t, pattern, splitter_opts) :: Enumerable.t()
def splitter(string, pattern, options \\ [])
def splitter(string, "", options) when is_binary(string) and is_list(options) do
@@ -1616,7 +1626,7 @@ defmodule String do
"é"
"""
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), keyword) :: t
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), replace_opts) :: t
def replace(subject, pattern, replacement, options \\ [])
when is_binary(subject) and
(is_binary(replacement) or is_function(replacement, 1)) and
+9 -3
View File
@@ -17,6 +17,11 @@ defmodule StringIO do
"""
@type open_opts :: [
capture_prompt: boolean(),
encoding: :unicode | :latin1
]
# We're implementing the GenServer behaviour instead of using the
# `use GenServer` macro, because we don't want the `child_spec/1`
# function as it doesn't make sense to be started under a supervisor.
@@ -59,7 +64,7 @@ defmodule StringIO do
"""
@doc since: "1.7.0"
@spec open(binary, keyword, (pid -> res)) :: {:ok, res} when res: var
@spec open(binary, open_opts, (pid -> res)) :: {:ok, res} when res: var
def open(string, options, function)
when is_binary(string) and is_list(options) and is_function(function, 1) do
{:ok, pid} = GenServer.start(__MODULE__, {self(), string, options}, [])
@@ -83,7 +88,8 @@ defmodule StringIO do
If options are provided, the result will be `{:ok, pid}`, returning the
IO device created. The option `:capture_prompt`, when set to `true`, causes
prompts (which are specified as arguments to `IO.get*` functions) to be
included in the device's output.
included in the device's output. See `options/3` for the list of supported
options.
If a function is provided, the device will be created and sent to the
function. When the function returns, the device will be closed. The final
@@ -111,7 +117,7 @@ defmodule StringIO do
{:ok, {"", "The input was foo"}}
"""
@spec open(binary, keyword) :: {:ok, pid}
@spec open(binary, open_opts) :: {:ok, pid}
@spec open(binary, (pid -> res)) :: {:ok, res} when res: var
def open(string, options_or_function \\ [])
+16 -2
View File
@@ -659,6 +659,19 @@ defmodule Supervisor do
@typedoc since: "1.16.0"
@type module_spec :: {module(), args :: term()} | module()
@typedoc """
Options for overriding child specification fields.
"""
@type child_spec_overrides :: [
id: atom() | term(),
start: {module(), atom(), [term()]},
restart: restart(),
shutdown: shutdown(),
type: type(),
modules: [module()] | :dynamic,
significant: boolean()
]
@doc """
Starts a supervisor with the given children.
@@ -896,7 +909,7 @@ defmodule Supervisor do
#=> start: {Agent, :start_link, [fn -> :ok end]}}
"""
@spec child_spec(child_spec() | module_spec(), keyword()) :: child_spec()
@spec child_spec(child_spec() | module_spec(), child_spec_overrides()) :: child_spec()
def child_spec(module_or_map, overrides)
def child_spec({_, _, _, _, _, _} = tuple, _overrides) do
@@ -1098,7 +1111,8 @@ defmodule Supervisor do
Returns a list with information about all children of the given supervisor.
Note that calling this function when supervising a large number of children
under low memory conditions can cause an out of memory exception.
under low memory conditions can bring the system down due to an out of memory
error.
This function returns a list of `{id, child, type, modules}` tuples, where:

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