Compare commits

..
Author SHA1 Message Date
José Valim 759443e724 Release v1.20.4 2026-08-28 12:02:55 +02:00
José Valim 0bba588757 Fix recursion on charlist error path 2026-08-28 11:59:47 +02:00
Alexander Gubarev 04c31907c5 Fix Task.yield_many/2 waits too long or forever when :limit exceeds task count (#15737) 2026-08-27 09:45:00 +02:00
José Valim 153489f9a6 Update CHANGELOG 2026-08-27 09:40:31 +02:00
Hasan mohamed 3ecb083d5d Fix type checker crash on non-returning for ... into: expression (#15747) (#15770) 2026-08-25 13:05:04 +02:00
Daniel Kukula d2689c839e Fix base64 swar singleton false positive (#15773) 2026-08-25 13:04:54 +02:00
Alexander Gubarev 309623adf1 Fix final sigma handling in :greek (#15791) 2026-08-25 13:04:14 +02:00
Łukasz Samson ae432598e4 Fix inferred type of :erlang.split_binary/2 result when arg is bitstring (#15790) 2026-08-25 13:04:08 +02:00
José Valim 31288d2267 Update CHANGELOG 2026-08-05 09:20:30 +02:00
José Valim 6efe0fc7cb Release v1.20.3 2026-08-04 16:31:51 +02:00
José Valim b3f2e0def7 Update CHANGELOG 2026-08-04 15:55:18 +02:00
Alexander Gubarev fa6205cc20 Fix elixirc --profile time switch argv parsing (#15647) 2026-08-04 15:49:27 +02:00
Łukasz Samson ea1519bcc0 Do not return {:ok, _} on maps with empty lines in map_put_static_value (#15597) 2026-08-04 15:19:29 +02:00
Łukasz Samson bfd4dd35fa Fix tuple_insert_static crash on non-normalized empty type (#15542)
Closes #15541.
2026-08-04 15:18:22 +02:00
Łukasz Samson 0564a597c6 Improve send return type (#15644)
Teach the type checker that Kernel.send/2 returns the message operand.
2026-08-04 15:16:47 +02:00
José Valim eded6248eb Optimize tuple and map emptiness checks (#15619)
Short-circuit disjoint and subtype tuple elements
before exploring the general difference branches.

This is a backport 59e257d3.

Assisted-by: Codex:GPT-5.6
2026-07-28 15:36:24 +02:00
José Valim 6bb46c30fc Update CHANGELOG 2026-07-19 13:21:16 +02:00
José Valim aa3dbdb289 Ensure %_{} is precise in guards, closes #15635 2026-07-19 10:06:36 +02:00
José Valim a55c1f7b6a Improve coverage of defguards 2026-07-17 16:20:35 +02:00
Łukasz Samson 76df4c6f58 Split tuple_insert_static into two paths (#15518)
- bdd_map on pure positive disjunction
- expand to the exact negation-free positive DNF, insert and union on negations and/or implicit :bdd_top

Fixes #15517
2026-07-16 17:50:51 +02:00
Alexander Gubarev 96f28d4362 Fix IEx.__break__!/3 fallback clause arity (#15595) 2026-07-12 13:52:34 +02:00
José Valim f894bc480e Provide proper signature for ++
And deal with compatibility checks in to_existing_atom/2.

Closes #15588.
2026-07-11 11:16:46 +02:00
José Valim 51d6cfdb93 Batch cachers in parallel checker (#15545) 2026-07-06 17:19:32 +02:00
José Valim 83ae474c61 Clarify protocols come later, closes #15529 2026-06-26 16:56:00 +02:00
Łukasz Samson 2fbf952cad Add missing Of.reset_vars call in receive/after badtimeout error path (#15527)
Variable refinement should not leak outside after block
2026-06-25 12:54:51 +02:00
Łukasz Samson 67772adcb2 Refer to the right function in remote_apply error on :maps.values/1 (#15526) 2026-06-25 12:54:51 +02:00
Łukasz Samson be1234799d Enable doctests on Descr, fix stale examples (#15528) 2026-06-25 12:54:51 +02:00
Łukasz Samson 4918e2d955 Thread full descr through fun_denormalize reduce (#15524)
Previously inner representation was threaded and returned, which broke expectations of caller `non_term_type_to_quoted`

Closes #15523.
2026-06-24 13:24:08 +02:00
José Valim cbf3836e8b Refine types for andalso/orelse in expressions, see #15450 2026-06-24 11:41:18 +02:00
Łukasz Samson 989165cabb Improve error returned from fun_apply (#15522)
- return badfun on empty type
- exclude call arity on badarity tuple

Fixes #15521

(cherry picked from commit 9a2029245d)
2026-06-24 10:34:45 +02:00
Guillaume Duboc 05d03543f6 Add put_dynamic for proper dynamic insertion (#15520) 2026-06-23 17:25:48 +02:00
José Valim 545dddf138 Release v1.20.2 2026-06-23 14:02:47 +02:00
Łukasz Samson bc30e7d696 Include bitstring in domain keys (#15516)
Fixes #15515
2026-06-23 10:44:27 +02:00
José Valim 73130012bb Better organize tests 2026-06-21 21:36:40 +02:00
Łukasz Samson f55abe5ff9 Correctly render list with negative leaves (#15514)
BDD leaves represent non empty lists

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

* Fix bin comprehensions - pinned var size

* Fix bin comprehensions - operations

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

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

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

Alternative approaches will be explored in
future releases.
2026-05-22 09:51:29 +02:00
José Valim 3a038d1763 Wrap quote in a function that will implement recursive types
Closes #15410.
2026-05-22 09:51:29 +02:00
José Valim e5e66831ac Avoid redundant emptiness checks on list/tuple emptiness (#15412) 2026-05-22 09:51:29 +02:00
José Valim 285c15f1ab Release v1.20.0-rc.6 2026-05-21 13:45:11 +02:00
José Valim 909903962d Branch out v1.20 2026-05-21 13:34:14 +02:00
308 changed files with 6114 additions and 11257 deletions
-4
View File
@@ -1,4 +0,0 @@
* Describe here the reasons behind the pull request.
* Make sure you have read the CONTRIBUTING.md file.
* Make sure any relevant documentation and tests have been added/updated.
* Do not submit Draft pull requests unless previously asked/agreed.
+4 -4
View File
@@ -42,11 +42,11 @@ jobs:
env: env:
ERLC_OPTS: "warnings_as_errors" ERLC_OPTS: "warnings_as_errors"
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 - uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with: with:
otp-version: ${{ matrix.otp_version }} otp-version: ${{ matrix.otp_version }}
@@ -123,11 +123,11 @@ jobs:
- name: Configure Git - name: Configure Git
run: git config --global core.autocrlf input run: git config --global core.autocrlf input
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 - uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with: with:
otp-version: ${{ matrix.otp_version }} otp-version: ${{ matrix.otp_version }}
+5 -5
View File
@@ -28,16 +28,16 @@ jobs:
build-mode: none build-mode: none
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
@@ -48,8 +48,8 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Checkout project - name: Checkout project
id: checkout id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
+2 -2
View File
@@ -33,9 +33,9 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Run markdownlint-cli2 - name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0 uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
+1 -1
View File
@@ -74,6 +74,6 @@ unless System.get_env("DRYRUN") do
"api-username" => "Elixir" "api-username" => "Elixir"
} }
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers) resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}") IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
end end
+2 -4
View File
@@ -89,9 +89,9 @@ runs:
- name: Run OSS Review Toolkit - name: Run OSS Review Toolkit
id: ort id: ort
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0 uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with: with:
image: ghcr.io/oss-review-toolkit/ort:92.2.0 image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >- run: >-
labels, labels,
cache-dependencies, cache-dependencies,
@@ -107,6 +107,4 @@ runs:
ort-cli-report-args: >- ort-cli-report-args: >-
-O CycloneDX=output.file.formats=json,xml -O CycloneDX=output.file.formats=json,xml
-O SpdxDocument=outputFileFormats=JSON,YAML -O SpdxDocument=outputFileFormats=JSON,YAML
ort-cli-scan-args: >-
--scanners Provenant
sw-version: "${{ inputs.version }}" sw-version: "${{ inputs.version }}"
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
+5 -5
View File
@@ -43,7 +43,7 @@ jobs:
--draft \ --draft \
"$GITHUB_REF_NAME" "$GITHUB_REF_NAME"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# zizmor: ignore[artipacked] # zizmor: ignore[artipacked]
if: github.ref_type == 'branch' if: github.ref_type == 'branch'
@@ -82,7 +82,7 @@ jobs:
otp_version: "29.0" otp_version: "29.0"
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
@@ -145,14 +145,14 @@ jobs:
- name: Log in to Azure - name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with: with:
client-id: ${{ secrets.AZURE_CLIENT_ID }} client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing" - name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with: with:
endpoint: https://eus.codesigning.azure.net/ endpoint: https://eus.codesigning.azure.net/
@@ -205,7 +205,7 @@ jobs:
- name: Checkout project - name: Checkout project
id: checkout id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
+2 -2
View File
@@ -17,11 +17,11 @@ jobs:
name: Notify name: Notify
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 - uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with: with:
otp-version: "27.3" otp-version: "27.3"
elixir-version: "1.18.3" elixir-version: "1.18.3"
+1 -2
View File
@@ -5,8 +5,7 @@
"**/*.md" "**/*.md"
], ],
"ignores": [ "ignores": [
".git/**", ".git/**"
".github/**"
], ],
"gitignore": true, "gitignore": true,
"config": { "config": {
-5
View File
@@ -37,11 +37,6 @@ curations:
comment: "Apply Trademark Policy to VERSION file" comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE" detected_license: "NONE"
concluded_license: "Apache-2.0" concluded_license: "Apache-2.0"
- path: ".github/pull_request_template.md"
reason: "NOT_DETECTED"
comment: "Apply default license to GitHub pull request template"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified # Wrongly Identified
- path: ".gitignore" - path: ".gitignore"
-1
View File
@@ -1 +0,0 @@
CONTRIBUTING.md
+394 -74
View File
@@ -4,108 +4,428 @@
SPDX-FileCopyrightText: 2012 Plataformatec SPDX-FileCopyrightText: 2012 Plataformatec
--> -->
# Changelog for Elixir v1.21 # Changelog for Elixir v1.20
## v1.21.0-dev This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements ## Type system improvements
#### EEx Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
* [EEx] Support splitting middle expressions across EEx clauses This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
#### Elixir ### Type inference of guards
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1` This release also performs inference of guards! Let's see some examples:
* [Code] Add support for the `:erlc_options` compiler option
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
* [Kernel] Improve performance of type constructors and complex intersections
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
* [Path] Add `Path.safe_join/2`
* [Registry] Optimize exact key matching in lookups
* [String] Optimize `String.bag_distance/2`
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
* [URI] Optimize percent-decoding and `URI.to_string/1`
#### ExUnit ```elixir
def example(x, y) when is_list(x) and is_integer(y)
```
* [ExUnit.Assertions] Add `trace/3` helper The code above correctly infers `x` is a list and `y` is an integer.
```elixir
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
```
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
```elixir
def example(x) when is_map_key(x, :foo)
```
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
```elixir
def example(x) when not is_map_key(x, :foo)
```
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
You can also have expressions that assert on the size of data structures:
```elixir
def example(x) when tuple_size(x) < 3
```
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
### Whole-body type inference
Elixir also performs inference based on the function body itself. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
### Typing across clauses
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
```elixir
case System.get_env("SOME_VAR") do
nil -> :not_found
value -> {:ok, String.upcase(value)}
end
```
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
### Typing of atom and domain keys in maps
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
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 code above has a type violation, which is now caught by the type system:
```text
warning: incompatible types given to User.name/1:
User.name(%{})
given types:
%{name: not_set()}
but expected one of:
dynamic(%{..., name: term()})
type warning found at:
│
16 │ User.name(%{})
│ ~
│
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
```
### Acknowledgements
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
## Compile-time improvements
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
* Errors during compilation may have less precise stacktraces
* Anonymous functions within `defmodule` can have only up to 20 arguments.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.4 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
### 2. Bug fixes ### 2. Bug fixes
#### Elixir #### Elixir
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y` * [Base] Fix false positives in Base64 validation
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3` * [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision * [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats * [String] Fix final sigma handling in Greek casing
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments * [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
* [Date.Range] Fix slicing date ranges with stepped ranges
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
* [Kernel] Fix inferred types for functions with non-returning clauses
* [Kernel] Fix map field type inference in the presence of empty map types
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
* [Kernel] Improve the error message for non-atom struct keys
* [Kernel] Raise when `|` is used in guards
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
* [Macro] Properly escape C1 control characters and Unicode noncharacters
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
#### ExUnit ## v1.20.3 (2026-08-04)
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas ### 1. Enhancements
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
#### Elixir
* [Kernel.ParallelCompiler] Batch type checker cache operations to improve compilation times
### 2. Bug fixes
#### Elixir
* [Kernel] Improve type precision of the `++/2` operator
* [Kernel] Improve type precision of the `send/2` return type
* [Kernel] Improve type refinement for `and/2` and `or/2`
* [Kernel] Fix type checking of `Map.put/3` on empty map types
* [Kernel] Fix type checking of tuple insertion on equivalent tuple types
* [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts
* [Kernel] Fix variable refinements leaking from invalid `receive/after` timeout expressions
* [Kernel] Fix rendering inferred types containing unions of static and dynamic functions
* [Kernel] Ensure typing errors in `:maps.values/1` reference the correct function
* [Kernel] Mark `%_{}` patterns as precise so subsequent redundant struct clauses are detected
* [elixirc] Fix parsing of the `--profile time` option
#### IEx #### IEx
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys * [IEx.Helpers] Fix `break!/1` with unknown expressions
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file ## v1.20.2 (2026-06-23)
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
### 1. Enhancements
#### Elixir
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
### 2. Bug fixes
#### Elixir
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
* [Module] Fix type checking when applying an empty function type
* [Module] Fix type checking of bitstring patterns that reuse variables
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
* [Module] Fix typing of list types involving dynamic or empty lists
* [Module] Include bitstrings as a possible domain key in the type system
* [Module] Preserve file metadata from each clause in type system warnings
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
#### Mix #### Mix
* [Mix] Prevent synchronization lock files from being overwritten with empty contents * [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4` * [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
* [mix new] Avoid trailing whitespace in generated files
### 3. Hard deprecations ## v1.20.1 (2026-06-09)
### 1. Security
#### Elixir #### Elixir
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead * [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)
### 4. Soft deprecations ### 2. Bug fixes
#### Elixir #### Elixir
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1` * [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1` * [Code] Ensure `Code.require_file` releases the file if compilation fails
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1` * [Kernel] Fix documentation generation to use the correct version in search
## v1.20 #### Mix
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md). * [mix archive.install] Validate paths and files when extracting archives
* [mix format] Honor `--no-compile` option when loading plugins
## v1.20.0 (2026-06-03)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### EEx
* [EEx] Optimize compiler by flattening expr list only once
#### Elixir
* [Base] Optimize Base validation functions by using SWAR techniques
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Code] Add `:dbg_callback` option to eval functions
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [Enum] Add `Enum.min_max` sorter
* [File] Add support for `[:raw]` opts in `File.read/2`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Float] Optimize `Float.round/2` by avoiding big integers
* [Inspect] Increase inspect limit to help print deeply nested data structures
* [Inspect] Support printing Erlang records (using Erlang notation)
* [Integer] Add `Integer.ceil_div/2`
* [Integer] Add `Integer.popcount/1`
* [IO] Add `IO.iodata_empty?/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Kernel] Detect and warn on redundant clauses
* [Kernel] Perform type inference across applications
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Kernel] Warn on unused requires
* [List] Add `List.first!/1` and `List.last!/1`
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
* Add Software Bill of Materials guide to the Documentation
#### ExUnit
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### IEx
* [IEx] Optimize autocompleting modules
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix app.tree] Support `--output` option
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
* [mix deps] Support filtering `mix deps` output
* [mix deps.tree] Support `--output` option
* [mix format] Support `--no-compile` option
* [mix help] Support printing docs for types and callbacks
* [mix source] Add `mix source MODULE` to print or open a given module/function location
* [mix test] Add `mix test --dry-run`
### 2. Potential breaking changes
#### Elixir
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
### 3. Bug fixes
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
* [File] Warn when defining `@type record()` for Erlang/OTP 29
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
* [Kernel] Reject negative Duration in `to_timeout/1`
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
* [String] Support empty pattern list in `String.count/2`
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
#### IEx
* [IEx] Ensure pry works across remote nodes
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
#### Logger
* [Logger] Persist log level to app env in `Logger.configure/1`
#### Mix
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile] Add a build lock around protocol consolidation in umbrellas
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
### 4. Hard deprecations
#### Elixir
* [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] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
## v1.19
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
+29 -25
View File
@@ -55,7 +55,7 @@ To recompile all (including Erlang modules):
make compile make compile
``` ```
After your changes are done, run `make format` to guarantee After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted, then run the full suite with all files are properly formatted, then run the full suite with
`make test`. `make test`.
@@ -71,39 +71,44 @@ updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml). [the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile). More tasks can be found by reading the [Makefile](Makefile).
## Sending a pull request We encourage contributors to write tests that capture both existing and newly
introduced behavior, especially for bug fixes and major changes:
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/) * **Bug Fixes:** If you are fixing a bug, please try to include a test that
and must include tests and other relevant proof of work: *fails* before your change and *passes* afterward. This makes it easier to
confirm that the fix addresses the underlying issue and helps prevent
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before regressions in the future.
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 * **New Features or Major Changes:** If you are adding a new feature or making
major changes to existing functionality, please add assocaited tests. Aim to major changes to existing functionality, please add tests that cover the
have the best code coverage possible. major parts of that functionality. Aim to have the best code coverage possible.
* **Performance improvements:** For performance improvements, please include the With tests running and passing, you are ready to contribute to Elixir and
benchmark script, with inputs and results, in the pull request description. [send a pull request](https://help.github.com/articles/using-pull-requests/).
We recommend using [benchee](https://github.com/bencheeorg/benchee). We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
Here are some pull requests we have received in the past you can use as reference: * [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992) * [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058) * [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes ## Reviewing changes
Once a pull request is sent, the Elixir team will review your changes. Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be reviewed before being merged into the repository.
If changes are necessary, the team will leave appropriate comments requesting If changes are necessary, the team will leave appropriate comments requesting
changes to the code. Unfortunately, we cannot guarantee a pull request will changes to the code. Unfortunately, we cannot guarantee a pull request will
be merged, even when modifications are requested, as the Elixir team will be merged, even when modifications are requested, as the Elixir team will
re-evaluate the contribution as it changes. re-evaluate the contribution as it changes.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
When the review finishes, your pull request will be squashed and merged When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in believe they should be merged without squashing, please mention it in
@@ -112,7 +117,7 @@ a comment.
## Building documentation ## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc) Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is cloned and compiled alongside Elixir. is installed and built alongside Elixir.
After cloning and compiling Elixir, run: After cloning and compiling Elixir, run:
@@ -130,7 +135,7 @@ make docs
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`, This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation, and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html). [please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
## Licensing and Compliance Requirements ## Licensing and Compliance Requirements
@@ -218,11 +223,10 @@ agents in Elixir spaces:
use coding agents to implement it, as long as it follows the AI Contributions use coding agents to implement it, as long as it follows the AI Contributions
guidelines below. guidelines below.
* When automating AI usage on the Elixir codebase for performance improvements, * When automating AI usage on the Elixir codebase for performance improvements
security fixes, or correctness changes to the compiler or type system, pair it or security fixes, pair it with a separate set of agents whose job is to argue
with a separate set of agents whose job is to argue against and try to invalidate against and try to invalidate any proposed change. And treat their approval as
any proposed change. And treat their approval as advisory: a human must still advisory: a human must still validate it before opening issues or pull requests.
validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below. If any code is written by AI, then you must follow the guidelines below.
+2 -2
View File
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs" TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/ # CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []' ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include ERLC := erlc -I lib/elixir/include
@@ -189,7 +189,7 @@ clean_elixir:
#==> Documentation tasks #==> Documentation tasks
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}") SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4) DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)" DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
+2 -2
View File
@@ -7,14 +7,14 @@
<h1> <h1>
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png"> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200"> <img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture> </picture>
</h1> </h1>
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain) [![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187)
Elixir is a programming language designed for building scalable Elixir is a dynamic, functional language designed for building scalable
and maintainable applications. and maintainable applications.
For more about Elixir, installation and documentation, For more about Elixir, installation and documentation,
-1
View File
@@ -12,7 +12,6 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.21 | Development
1.20 | Bug fixes and security patches 1.20 | Bug fixes and security patches
1.19 | Security patches only 1.19 | Security patches only
1.18 | Security patches only 1.18 | Security patches only
+1 -1
View File
@@ -1 +1 @@
1.21.0-dev 1.20.4
+1 -1
View File
@@ -6,7 +6,7 @@
set -e set -e
ELIXIR_VERSION=1.21.0-dev ELIXIR_VERSION=1.20.4
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2 cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team :: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec :: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.21.0-dev set ELIXIR_VERSION=1.20.4
if ""%1""=="""" if ""%2""=="""" goto documentation if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+2 -3
View File
@@ -293,7 +293,7 @@ defmodule EEx do
"foo baz" "foo baz"
""" """
@spec eval_string(String.t(), Code.binding(), [compile_opt]) :: term() @spec eval_string(String.t(), keyword, [compile_opt]) :: term()
def eval_string(source, bindings \\ [], options \\ []) def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options) compiled = compile_string(source, options)
@@ -315,7 +315,7 @@ defmodule EEx do
#=> "foo baz" #=> "foo baz"
""" """
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t() @spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
def eval_file(filename, bindings \\ [], options \\ []) def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename) filename = IO.chardata_to_string(filename)
@@ -344,7 +344,6 @@ defmodule EEx do
It returns `{:ok, [token]}` where a token is one of: It returns `{:ok, [token]}` where a token is one of:
* `{:comment, content, %{column: column, line: line}}`
* `{:text, content, %{column: column, line: line}}` * `{:text, content, %{column: column, line: line}}`
* `{:expr, marker, content, %{column: column, line: line}}` * `{:expr, marker, content, %{column: column, line: line}}`
* `{:start_expr, marker, content, %{column: column, line: line}}` * `{:start_expr, marker, content, %{column: column, line: line}}`
+18 -47
View File
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
{:error, message <> code_snippet(state.source, state.indentation, meta), meta} {:error, message <> code_snippet(state.source, state.indentation, meta), meta}
{:ok, expr, new_line, new_column, rest} -> {:ok, expr, new_line, new_column, rest} ->
{key, expr, extra_meta} = {key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} -> {:ok, _line, _column, _warnings, rev_tokens, []} ->
# We ignore warnings because the code will be tokenized # We ignore warnings because the code will be tokenized
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
token_key(rev_tokens, expr) token_key(rev_tokens, expr)
{:error, _, _, _, _} -> {:error, _, _, _, _} ->
{:expr, expr, %{}} {:expr, expr}
end end
marker = marker =
@@ -102,8 +102,8 @@ defmodule EEx.Compiler do
marker marker
end end
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)} token = {key, marker, expr, %{line: line, column: column}}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1)) trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
end end
end end
@@ -127,27 +127,6 @@ defmodule EEx.Compiler do
tokenize(rest, line, column, state, [{line, column}], fun.(acc)) tokenize(rest, line, column, state, [{line, column}], fun.(acc))
end end
# Merge middle expressions separated only by whitespace so the whitespace is
# part of the Elixir expression, not a separate EEx body.
defp merge_token(
{:middle_expr, ~c"", chars, meta},
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
) do
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
else
[
{:middle_expr, ~c"", chars, meta},
{:text, text, text_meta},
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
]
end
end
defp merge_token(token, acc) do
[token | acc]
end
# Retrieve marker for <% # Retrieve marker for <%
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
@@ -198,37 +177,35 @@ defmodule EEx.Compiler do
defp token_key(rev_tokens, expr) do defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
{[{:end, _} | _], [{:do, _} | _]} -> {[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr, %{}} {:middle_expr, expr}
{_, [{:do, _} | _]} -> {_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr), %{}} {:start_expr, maybe_append_space(expr)}
{_, [{:block_identifier, _, identifier} | _]} -> {_, [{:block_identifier, _, _} | _]} ->
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}} {:middle_expr, maybe_append_space(expr)}
{[{:end, _} | _], [{:stab_op, _, _} | _]} -> {[{:end, _} | _], [{:stab_op, _, _} | _]} ->
{:middle_expr, expr, %{}} {:middle_expr, expr}
{_, [{:stab_op, _, _} | rev_tokens]} -> {_, [{:stab_op, _, _} | reverse_tokens]} ->
if fn_before_end?(rev_tokens) do fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
{:start_expr, expr, %{}} end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if end_index > fn_index do
{:start_expr, expr}
else else
{:middle_expr, expr, %{}} {:middle_expr, expr}
end end
{tokens, _} -> {tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> {:end_expr, expr, %{}} [{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr, %{}} _ -> {:expr, expr}
end end
end end
end end
defp fn_before_end?([{:fn, _} | _]), do: true
defp fn_before_end?([{:end, _} | _]), do: false
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
defp fn_before_end?([]), do: false
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest) defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
defp drop_eol(rest), do: rest defp drop_eol(rest), do: rest
@@ -499,12 +476,6 @@ defmodule EEx.Compiler do
Enum.all?(chars, &(&1 in @all_spaces)) Enum.all?(chars, &(&1 in @all_spaces))
end end
defp clause_block_identifier?(%{block_identifier: identifier}) do
identifier in [:else, :rescue, :catch]
end
defp clause_block_identifier?(_meta), do: false
# Changes placeholder to real expression # Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do defp insert_quoted({:__EEX__, _, [key]}, quoted) do
+1 -1
View File
@@ -195,7 +195,7 @@ defmodule EEx.Engine do
def handle_expr(state, "=", ast) do def handle_expr(state, "=", ast) do
check_state!(state) check_state!(state)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state %{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__) var = Macro.var(:"arg#{vars_count}", __MODULE__)
ast = ast =
quote do quote do
+2 -2
View File
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
{:text, ~c"foo ", %{column: 1, line: 1}}, {:text, ~c"foo ", %{column: 1, line: 1}},
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}}, {:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
{:text, ~c"bar", %{column: 21, line: 1}}, {:text, ~c"bar", %{column: 21, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}}, {:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
{:text, ~c"baz", %{column: 34, line: 1}}, {:text, ~c"baz", %{column: 34, line: 1}},
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}}, {:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
{:eof, %{column: 46, line: 1}} {:eof, %{column: 46, line: 1}}
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
exprs = [ exprs = [
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}}, {:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}}, {:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}}, {:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}}, {:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}}, {:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
{:eof, %{column: 3, line: 7}} {:eof, %{column: 3, line: 7}}
+1 -32
View File
@@ -262,37 +262,6 @@ defmodule EExTest do
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>") assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
end end
test "with expression with else clause split across tags" do
template = """
<%= with {:ok, x} <- @res do %>
<p><%= x %></p>
<% else %>
<% _ -> %>
<p>bad</p>
<% end %>
"""
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
engine: EEx.SmartEngine
)
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
engine: EEx.SmartEngine
)
end
test "empty clauses separated by whitespace" do
template = """
<%= case x do %>
<% :foo -> %>
<% :bar -> %>
<% end %>
"""
assert_eval("\n \n", template, x: :foo)
assert_eval("\n\n", template, x: :bar)
end
test "with end of token" do test "with end of token" do
assert_eval("foo bar %>", "foo bar %>") assert_eval("foo bar %>", "foo bar %>")
end end
@@ -553,7 +522,7 @@ defmodule EExTest do
assert message |> Exception.message() |> strip_ansi() =~ """ assert message |> Exception.message() |> strip_ansi() =~ """
│ │
#{line + 2} │ true && @some[\s 514 │ true && @some[\s
│ │ └ missing closing delimiter (expected "]") │ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter │ └ unclosed delimiter
""" """
+30 -86
View File
@@ -10,8 +10,8 @@ defmodule Access do
keys of any type in a data structure via the `data[key]` syntax. keys of any type in a data structure via the `data[key]` syntax.
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out `Access` supports keyword lists (`Keyword`) and maps (`Map`) out
of the box. Keyword lists support only atom keys, while keys for maps of the box. Keywords supports only atoms keys, keys for maps can
can be of any type. Both return `nil` if the key does not exist: be of any type. Both return `nil` if the key does not exist:
iex> keywords = [a: 1, b: 2] iex> keywords = [a: 1, b: 2]
iex> keywords[:a] iex> keywords[:a]
@@ -33,7 +33,7 @@ defmodule Access do
iex> keywords[:c][:unknown] iex> keywords[:c][:unknown]
nil nil
This works because accessing anything on a `nil` value returns This works because accessing anything on a `nil` value, returns
`nil` itself: `nil` itself:
iex> nil[:a] iex> nil[:a]
@@ -226,8 +226,6 @@ defmodule Access do
end end
end end
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
@doc """ @doc """
Fetches the value for the given key in a container (a map, keyword Fetches the value for the given key in a container (a map, keyword
list, or struct that implements the `Access` behaviour). list, or struct that implements the `Access` behaviour).
@@ -487,7 +485,7 @@ defmodule Access do
## Accessors ## Accessors
@doc """ @doc """
Returns a function that accesses the given key in a map/struct/keyword list. Returns a function that accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`, The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends. `Kernel.get_and_update_in/3`, and friends.
@@ -516,56 +514,30 @@ defmodule Access do
iex> pop_in(map, [Access.key(:user), Access.key(:name)]) iex> pop_in(map, [Access.key(:user), Access.key(:name)])
{"john", %{user: %{}}} {"john", %{user: %{}}}
iex> keyword = [user: [name: "john"]] An error is raised if the accessed structure is not a map or a struct:
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
"john"
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
{"john", [user: []]}
An error is raised if the accessed structure is not a map, struct, or keyword list: iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got:
iex> get_in(123, [Access.key(:foo)]) ...
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
""" """
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term) @spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
def key(key, default \\ nil) do def key(key, default \\ nil) do
fn fn
:get, %{} = data, next -> :get, data, next ->
next.(Map.get(data, key, default)) next.(Map.get(data, key, default))
:get_and_update, %{} = data, next -> :get_and_update, data, next ->
value = Map.get(data, key, default) value = Map.get(data, key, default)
case next.(value) do case next.(value) do
{get, update} -> {get, Map.put(data, key, update)} {get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)} :pop -> {value, Map.delete(data, key)}
end end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.get(data, key, default))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.get(data, key, default)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next ->
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
end end
end end
@doc """ @doc """
Returns a function that accesses the given key in a map/struct/keyword list. Returns a function that accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`, The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends. `Kernel.get_and_update_in/3`, and friends.
@@ -574,19 +546,6 @@ defmodule Access do
## Examples ## Examples
iex> keyword = [user: [name: "john"]]
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
"john"
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
{"john", [user: []]}
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in:
...
iex> map = %{user: %{name: "john"}} iex> map = %{user: %{name: "john"}}
iex> get_in(map, [Access.key!(:user), Access.key!(:name)]) iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
"john" "john"
@@ -615,15 +574,13 @@ defmodule Access do
`Access.key!/1` is useful when the key is not known in advance `Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically. and must be accessed dynamically.
An error is raised if the accessed structure is not a map/struct/keyword list: An error is raised if the accessed structure is not a map/struct:
iex> get_in(123, [Access.key!(:foo)]) iex> get_in([], [Access.key!(:foo)])
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123 ** (RuntimeError) Access.key!/1 expected a map/struct, got: []
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
""" """
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term) @spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
def key!(key) do def key!(key) do
fn fn
:get, %{} = data, next -> :get, %{} = data, next ->
@@ -637,19 +594,8 @@ defmodule Access do
:pop -> {value, Map.delete(data, key)} :pop -> {value, Map.delete(data, key)}
end end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.fetch!(data, key))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.fetch!(data, key)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next -> _op, data, _next ->
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}" raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
end end
end end
@@ -917,7 +863,7 @@ defmodule Access do
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name]) iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]} {["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
When no match is found, an empty list is returned and the update function is never called: When no match is found, an empty list is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}] iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name]) iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
@@ -934,7 +880,7 @@ defmodule Access do
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list) @spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
def filter(func) when is_function(func) do def filter(func) when is_function(func) do
fn op, data, next -> filter(op, data, func, next) end fn op, data, next -> filter(op, data, func, next) end
end end
@@ -1035,12 +981,12 @@ defmodule Access do
end end
defp slice(:get_and_update, data, range, next) when is_list(data) do defp slice(:get_and_update, data, range, next) when is_list(data) do
%Range{first: first, last: last, step: step} = normalize_range(range, data) range = normalize_range(range, data)
if first > last do if range.first > range.last do
{[], data} {[], data}
else else
get_and_update_slice(data, first, last, step, next, [], [], 0) get_and_update_slice(data, range, next, [], [], 0)
end end
end end
@@ -1145,23 +1091,21 @@ defmodule Access do
defp normalize_range(range, _list), do: range defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index) defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
when index > last do when index > last do
{:lists.reverse(gets), :lists.reverse(updates, rest)} {:lists.reverse(gets), :lists.reverse(updates, rest)}
end end
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
if index >= first and rem(index - first, step) == 0 do if index in range do
case next.(head) do case next.(head) do
:pop -> :pop ->
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1) get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
{get, update} -> {get, update} ->
get_and_update_slice( get_and_update_slice(
rest, rest,
first, range,
last,
step,
next, next,
[update | updates], [update | updates],
[get | gets], [get | gets],
@@ -1169,11 +1113,11 @@ defmodule Access do
) )
end end
else else
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1) get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
end end
end end
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)} {:lists.reverse(gets), :lists.reverse(updates)}
end end
@@ -1200,7 +1144,7 @@ defmodule Access do
iex> pop_in(list, [Access.find(&(&1.salary <= 40))]) iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]} {%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called: When no match is found, nil is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}] iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name]) iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
+3 -3
View File
@@ -327,7 +327,7 @@ defmodule Agent do
passing the agent state. The result of the function invocation is passing the agent state. The result of the function invocation is
returned from this function. returned from this function.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
@@ -366,7 +366,7 @@ defmodule Agent do
elements, the first being the value to return (that is, the "get" value) elements, the first being the value to return (that is, the "get" value)
and the second one being the new state of the agent. and the second one being the new state of the agent.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
@@ -407,7 +407,7 @@ defmodule Agent do
This function always returns `:ok`. This function always returns `:ok`.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
+2 -2
View File
@@ -996,7 +996,7 @@ defmodule Application do
end end
@doc """ @doc """
Gets the directory for `app`. Gets the directory for app.
This information is returned based on the code path. Here is an This information is returned based on the code path. Here is an
example: example:
@@ -1077,7 +1077,7 @@ defmodule Application do
@doc """ @doc """
Formats the error reason returned by `start/2`, Formats the error reason returned by `start/2`,
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`, `ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
and returns a string. returns a string.
""" """
@spec format_error(any) :: String.t() @spec format_error(any) :: String.t()
def format_error(reason) do def format_error(reason) do
+19 -19
View File
@@ -436,7 +436,7 @@ defmodule Base do
end end
for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do
name = String.to_unsafe_atom("encode16#{base}") name = :"encode16#{base}"
encoded = to_encode_list.(alphabet) encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]} @compile {:inline, [{name, 1}]}
@@ -536,7 +536,7 @@ defmodule Base do
* `:lower` - only allows lower case characters * `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters * `:mixed` - allows mixed case characters
An `ArgumentError` exception is raised if the string has an odd length or An `ArgumentError` exception is raised if the padding is incorrect or
a non-alphabet character is present in the string. a non-alphabet character is present in the string.
## Examples ## Examples
@@ -617,10 +617,10 @@ defmodule Base do
upper = Enum.with_index(b16_alphabet) upper = Enum.with_index(b16_alphabet)
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
decode_name = String.to_unsafe_atom("decode16#{base}!") decode_name = :"decode16#{base}!"
validate_name = String.to_unsafe_atom("validate16#{base}?") validate_name = :"validate16#{base}?"
valid_char_name = String.to_unsafe_atom("valid_char16#{base}?") valid_char_name = :"valid_char16#{base}?"
valid_word_name = String.to_unsafe_atom("valid_word16#{base}?") valid_word_name = :"valid_word16#{base}?"
{min, decoded} = to_decode_list.(alphabet) {min, decoded} = to_decode_list.(alphabet)
@@ -735,7 +735,7 @@ defmodule Base do
end end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
name = String.to_unsafe_atom("encode64#{base}") name = :"encode64#{base}"
encoded = to_encode_list.(alphabet) encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]} @compile {:inline, [{name, 1}]}
@@ -1001,12 +1001,12 @@ defmodule Base do
end end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
decode_name = String.to_unsafe_atom("decode64#{base}!") decode_name = :"decode64#{base}!"
validate_name = String.to_unsafe_atom("validate64#{base}?") validate_name = :"validate64#{base}?"
validate_main_name = String.to_unsafe_atom("validate_main64#{validate_name}?") validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = String.to_unsafe_atom("valid_char64#{base}?") valid_char_name = :"valid_char64#{base}?"
valid_word_name = String.to_unsafe_atom("valid_word64#{base}?") valid_word_name = :"valid_word64#{base}?"
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.() {min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?` # SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
@@ -1314,7 +1314,7 @@ defmodule Base do
hexupper: b32hex_alphabet, hexupper: b32hex_alphabet,
hexlower: to_lower_enc.(b32hex_alphabet) hexlower: to_lower_enc.(b32hex_alphabet)
] do ] do
name = String.to_unsafe_atom("encode32#{base}") name = :"encode32#{base}"
encoded = to_encode_list.(alphabet) encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]} @compile {:inline, [{name, 1}]}
@@ -1388,7 +1388,7 @@ defmodule Base do
The values for `:case` can be: The values for `:case` can be:
* `:upper` - only allows upper case characters (default) * `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters * `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters * `:mixed` - allows mixed case characters
@@ -1653,16 +1653,16 @@ defmodule Base do
hexlower: to_lower_dec.(hexupper), hexlower: to_lower_dec.(hexupper),
hexmixed: to_mixed_dec.(hexupper) hexmixed: to_mixed_dec.(hexupper)
] do ] do
decode_name = String.to_unsafe_atom("decode32#{base}!") decode_name = :"decode32#{base}!"
validate_name = String.to_unsafe_atom("validate32#{base}?") validate_name = :"validate32#{base}?"
validate_main_name = String.to_unsafe_atom("validate_main32#{validate_name}?") validate_main_name = :"validate_main32#{validate_name}?"
valid_char_name = String.to_unsafe_atom("valid_char32#{base}?") valid_char_name = :"valid_char32#{base}?"
{min, decoded} = to_decode_list.(alphabet) {min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?` # SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an # in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause. # 8-byte-multiple `main`) recurses through the single-byte clause.
valid_word_name = String.to_unsafe_atom("valid_word32#{base}?") valid_word_name = :"valid_word32#{base}?"
defp unquote(validate_main_name)(<<w::56, rest::binary>>), defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest) do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
+27 -47
View File
@@ -106,7 +106,6 @@ defmodule Calendar do
@typedoc "Any map or struct that contains the time fields." @typedoc "Any map or struct that contains the time fields."
@type time :: %{ @type time :: %{
optional(any) => any, optional(any) => any,
calendar: calendar,
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, second: second,
@@ -296,7 +295,7 @@ defmodule Calendar do
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond} @callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
@doc """ @doc """
Defines the rollover moment for the calendar. Define the rollover moment for the calendar.
This is the moment, in your calendar, when the current day ends This is the moment, in your calendar, when the current day ends
and the next day starts. and the next day starts.
@@ -382,13 +381,13 @@ defmodule Calendar do
@callback iso_days_to_end_of_day(iso_days) :: iso_days @callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """ @doc """
Shifts date by the given duration according to its calendar. Shifts date by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day} @callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """ @doc """
Shifts naive datetime by the given duration according to its calendar. Shifts naive datetime by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_naive_datetime( @callback shift_naive_datetime(
@@ -403,7 +402,7 @@ defmodule Calendar do
) :: {year, month, day, hour, minute, second, microsecond} ) :: {year, month, day, hour, minute, second, microsecond}
@doc """ @doc """
Shifts time by the given duration according to its calendar. Shifts time by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) :: @callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
@@ -563,11 +562,11 @@ defmodule Calendar do
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
q | Quarter | 1, 2, 3, 4 q | Quarter | 1, 2, 3, 4
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877 s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
S | Second | 00, 59 S | Second | 00, 59, 60
u | Day of the week | 1 (Monday), 7 (Sunday) u | Day of the week | 1 (Monday), 7 (Sunday)
x | Preferred date (without time) representation | 2018-10-17 x | Preferred date (without time) representation | 2018-10-17
X | Preferred time (without date) representation | 12:34:56 X | Preferred time (without date) representation | 12:34:56
y | Year as 2-digits | -01, 01, 86, 18 y | Year as 2-digits | 01, 01, 86, 18
Y | Year | -0001, 0001, 1986 Y | Year | -0001, 0001, 1986
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530 z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
Z | Time zone abbreviation (empty string if naive) | CET, BRST Z | Time zone abbreviation (empty string if naive) | CET, BRST
@@ -624,7 +623,7 @@ defmodule Calendar do
...>) ...>)
"серпень" "серпень"
Microsecond formatting: Microsecond formatting:
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f") iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.0" "19-08-26 13:52:06.0"
@@ -712,7 +711,7 @@ defmodule Calendar do
# Literally just % # Literally just %
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc]) parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
end end
# Abbreviated name of day # Abbreviated name of day
@@ -781,7 +780,7 @@ defmodule Calendar do
# Day of the month # Day of the month
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -800,35 +799,31 @@ defmodule Calendar do
# Hour using a 24-hour clock # Hour using a 24-hour clock
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Hour using a 12-hour clock # Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Day of the year # Day of the year
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Month # Month
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Minute # Minute
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -856,23 +851,19 @@ defmodule Calendar do
# Quarter # Quarter
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Second # Second
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Day of the week # Day of the week
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -922,25 +913,20 @@ defmodule Calendar do
# Year as 2-digits # Year as 2-digits
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
if datetime.year < 0 do
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
else
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
end
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Year # Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = {sign, year} =
if datetime.year < 0 do if datetime.year < 0 do
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)] {?-, -datetime.year}
else else
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad) {[], datetime.year}
end end
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -987,7 +973,7 @@ defmodule Calendar do
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60)) Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
sign = if utc_offset + std_offset >= 0, do: "+", else: "-" sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}" result = "#{sign}#{pad_leading(offset_number, width, pad)}"
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -1006,19 +992,13 @@ defmodule Calendar do
raise ArgumentError, "invalid strftime format: %#{next}" raise ArgumentError, "invalid strftime format: %#{next}"
end end
defp pad_preferred(result, width, pad) do defp pad_preferred(result, width, pad) when length(result) < width do
result pad_preferred([pad | result], width, pad)
|> IO.iodata_to_binary()
|> pad_leading(width, pad)
end end
defp pad_preferred(result, _width, _pad), do: result
defp pad_leading(string, count, padding) do defp pad_leading(string, count, padding) do
to_pad = count - String.length(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
defp pad_leading_ascii(string, count, padding) do
to_pad = count - byte_size(string) to_pad = count - byte_size(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end end
+12 -16
View File
@@ -81,7 +81,7 @@ defmodule Date do
Ranges of dates can be increasing (`first <= last`) and are Ranges of dates can be increasing (`first <= last`) and are
always inclusive. For a decreasing range, use `range/3` with always inclusive. For a decreasing range, use `range/3` with
a step of -1 as third argument. a step of -1 as first argument.
## Examples ## Examples
@@ -160,7 +160,7 @@ defmodule Date do
) do ) do
raise ArgumentError, raise ArgumentError,
"both dates must have matching calendar and the step must be a " <> "both dates must have matching calendar and the step must be a " <>
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}" "non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
end end
defp range(first, first_days, last, last_days, calendar, step) do defp range(first, first_days, last, last_days, calendar, step) do
@@ -422,7 +422,7 @@ defmodule Date do
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
date date
|> convert!(Calendar.ISO) |> convert!(Calendar.ISO)
|> to_iso8601(format) |> to_iso8601()
end end
@doc """ @doc """
@@ -556,18 +556,14 @@ defmodule Date do
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt @spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
def compare( def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
%{year: year1, month: month1, day: day1, calendar: calendar}, %{year: year1, month: month1, day: day1} = date1
%{year: year2, month: month2, day: day2, calendar: calendar} %{year: year2, month: month2, day: day2} = date2
) do
cond do case {{year1, month1, day1}, {year2, month2, day2}} do
year1 > year2 -> :gt {first, second} when first > second -> :gt
year1 < year2 -> :lt {first, second} when first < second -> :lt
month1 > month2 -> :gt _ -> :eq
month1 < month2 -> :lt
day1 > day2 -> :gt
day1 < day2 -> :lt
true -> :eq
end end
end end
@@ -1051,7 +1047,7 @@ defmodule Date do
@doc """ @doc """
Calculates the quarter of the year of a given `date`. Calculates the quarter of the year of a given `date`.
Returns the quarter of the year as an integer. For the ISO 8601 Returns the day of the year as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 4. calendar (the default), it is an integer from 1 to 4.
## Examples ## Examples
+39 -10
View File
@@ -37,19 +37,21 @@ defmodule Date.Range do
first_in_iso_days: first_days, first_in_iso_days: first_days,
last_in_iso_days: last_days, last_in_iso_days: last_days,
step: step step: step
}, } = range,
%Date{calendar: calendar} = date %Date{calendar: calendar} = date
) do ) do
{days, _} = Date.to_iso_days(date) {days, _} = Date.to_iso_days(date)
in_range? = cond do
if step > 0 do empty?(range) ->
first_days <= days and days <= last_days and rem(days - first_days, step) == 0 {:ok, false}
else
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
end
{:ok, in_range?} first_days <= last_days ->
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
true ->
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
end
end end
def member?(%Date.Range{step: _}, _) do def member?(%Date.Range{step: _}, _) do
@@ -86,7 +88,7 @@ defmodule Date.Range do
step: step step: step
} = range } = range
) do ) do
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)} {:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
@@ -183,7 +185,7 @@ defmodule Date.Range do
last_in_iso_days: last_days, last_in_iso_days: last_days,
step: step step: step
}), }),
do: div(last_days - first_days, step) + 1 do: abs(div(last_days - first_days, step)) + 1
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
defp size( defp size(
@@ -193,6 +195,33 @@ defmodule Date.Range do
step = if first_days <= last_days, do: 1, else: -1 step = if first_days <= last_days, do: 1, else: -1
size(Map.put(date_range, :step, step)) size(Map.put(date_range, :step, step))
end end
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step > 0 and first_days > last_days,
do: true
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step < 0 and first_days < last_days,
do: true
defp empty?(%Date.Range{step: _}), do: false
# TODO: Remove me on v2.0
defp empty?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
empty?(Map.put(date_range, :step, step))
end
end end
defimpl Inspect do defimpl Inspect do
+47 -41
View File
@@ -180,7 +180,7 @@ defmodule DateTime do
since v1.15.0. since v1.15.0.
The default unit if none gets passed is `:native`, The default unit if none gets passed is `:native`,
which results in a default resolution of microseconds. which results on a default resolution of microseconds.
## Examples ## Examples
@@ -720,9 +720,6 @@ defmodule DateTime do
Other time zone databases can be passed as argument or set globally. Other time zone databases can be passed as argument or set globally.
See the "Time zone database" section in the module docs. See the "Time zone database" section in the module docs.
Shifting to the `"Etc/UTC"` time zone always succeeds without
consulting the `time_zone_database`.
## Examples ## Examples
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase) iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
@@ -756,28 +753,6 @@ defmodule DateTime do
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database) |> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
end end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days_utc)
datetime = %DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
{:ok, datetime}
end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} -> {:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
@@ -1202,7 +1177,7 @@ defmodule DateTime do
datetime datetime
|> to_iso_days() |> to_iso_days()
# Subtract total original offset in order to get UTC and add the new offset # Subtract total original offset in order to get UTC and add the new offset
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second) |> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|> calendar.naive_datetime_from_iso_days() |> calendar.naive_datetime_from_iso_days()
end end
@@ -1321,9 +1296,9 @@ defmodule DateTime do
end end
@doc """ @doc """
Converts a number of Gregorian seconds to a `DateTime` struct. Converts a number of gregorian seconds to a `DateTime` struct.
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
`DateTime.shift_zone/3`. `DateTime.shift_zone/3`.
## Examples ## Examples
@@ -1366,7 +1341,7 @@ defmodule DateTime do
end end
@doc """ @doc """
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds. Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
## Examples ## Examples
@@ -1601,15 +1576,15 @@ defmodule DateTime do
def diff(datetime1, datetime2, unit \\ :second) def diff(datetime1, datetime2, unit \\ :second)
def diff(datetime1, datetime2, :day) do def diff(datetime1, datetime2, :day) do
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000) diff(datetime1, datetime2, :second) |> div(86400)
end end
def diff(datetime1, datetime2, :hour) do def diff(datetime1, datetime2, :hour) do
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000) diff(datetime1, datetime2, :second) |> div(3600)
end end
def diff(datetime1, datetime2, :minute) do def diff(datetime1, datetime2, :minute) do
diff(datetime1, datetime2, :microsecond) |> div(60_000_000) diff(datetime1, datetime2, :second) |> div(60)
end end
def diff( def diff(
@@ -1651,9 +1626,6 @@ defmodule DateTime do
all converted to microseconds. Negative values will move backwards all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`. in time and the default precision is `:second`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This function relies on a contiguous representation of time, This function relies on a contiguous representation of time,
ignoring timezone changes. For example, if you add one day when there ignoring timezone changes. For example, if you add one day when there
are summer time/daylight saving time changes, it will also change the are summer time/daylight saving time changes, it will also change the
@@ -1772,9 +1744,6 @@ defmodule DateTime do
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This operation is equivalent to shifting the datetime wall clock This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see (in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it on their watch), then applying the time zone offset to convert it
@@ -1843,6 +1812,44 @@ defmodule DateTime do
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t @spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{ %{
year: year, year: year,
@@ -2053,12 +2060,11 @@ defmodule DateTime do
end end
defp apply_tz_offset(iso_days, offset) do defp apply_tz_offset(iso_days, offset) do
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second) Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
end end
defp from_map(%{} = datetime_map) do defp from_map(%{} = datetime_map) do
%DateTime{ %DateTime{
calendar: datetime_map.calendar,
year: datetime_map.year, year: datetime_map.year,
month: datetime_map.month, month: datetime_map.month,
day: datetime_map.day, day: datetime_map.day,
+39 -53
View File
@@ -72,7 +72,7 @@ defmodule Duration do
However, once again, it is important to remember that shifting a duration is not However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what arithmetic, so you may want to use the functions in this module depending on what
you want to achieve. Compare the results of both examples below: you to achieve. Compare the results of both examples below:
# Adding one month after the other # Adding one month after the other
iex> date = ~D[2016-01-31] iex> date = ~D[2016-01-31]
@@ -129,16 +129,6 @@ defmodule Duration do
second: 0, second: 0,
microsecond: {0, 0} microsecond: {0, 0}
@typedoc """
The microsecond component of a duration.
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
durations may represent negative amounts of time. The precision is
an integer from 0 to 6 holding the number of significant digits,
as in the calendar types.
"""
@type microsecond :: {value :: integer, precision :: 0..6}
@typedoc """ @typedoc """
The duration struct type. The duration struct type.
""" """
@@ -150,7 +140,7 @@ defmodule Duration do
hour: integer, hour: integer,
minute: integer, minute: integer,
second: integer, second: integer,
microsecond: microsecond() microsecond: Calendar.microsecond()
} }
@typedoc """ @typedoc """
@@ -164,7 +154,7 @@ defmodule Duration do
| {:hour, integer} | {:hour, integer}
| {:minute, integer} | {:minute, integer}
| {:second, integer} | {:second, integer}
| {:microsecond, microsecond()} | {:microsecond, Calendar.microsecond()}
@typedoc """ @typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs. The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
@@ -253,19 +243,19 @@ defmodule Duration do
""" """
@spec add(t, t) :: t @spec add(t, t) :: t
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do def add(%Duration{} = d1, %Duration{} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1 {m1, p1} = d1.microsecond
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2 {m2, p2} = d2.microsecond
%Duration{ %Duration{
year: y1 + y2, year: d1.year + d2.year,
month: mo1 + mo2, month: d1.month + d2.month,
week: w1 + w2, week: d1.week + d2.week,
day: day1 + day2, day: d1.day + d2.day,
hour: h1 + h2, hour: d1.hour + d2.hour,
minute: mi1 + mi2, minute: d1.minute + d2.minute,
second: s1 + s2, second: d1.second + d2.second,
microsecond: {ms1 + ms2, max(p1, p2)} microsecond: {m1 + m2, max(p1, p2)}
} }
end end
@@ -283,19 +273,19 @@ defmodule Duration do
""" """
@spec subtract(t, t) :: t @spec subtract(t, t) :: t
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do def subtract(%Duration{} = d1, %Duration{} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1 {m1, p1} = d1.microsecond
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2 {m2, p2} = d2.microsecond
%Duration{ %Duration{
year: y1 - y2, year: d1.year - d2.year,
month: mo1 - mo2, month: d1.month - d2.month,
week: w1 - w2, week: d1.week - d2.week,
day: day1 - day2, day: d1.day - d2.day,
hour: h1 - h2, hour: d1.hour - d2.hour,
minute: mi1 - mi2, minute: d1.minute - d2.minute,
second: s1 - s2, second: d1.second - d2.second,
microsecond: {ms1 - ms2, max(p1, p2)} microsecond: {m1 - m2, max(p1, p2)}
} }
end end
@@ -312,16 +302,14 @@ defmodule Duration do
""" """
@spec multiply(t, integer) :: t @spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{ %Duration{
year: y * integer, year: duration.year * integer,
month: mo * integer, month: duration.month * integer,
week: w * integer, week: duration.week * integer,
day: d * integer, day: duration.day * integer,
hour: h * integer, hour: duration.hour * integer,
minute: mi * integer, minute: duration.minute * integer,
second: s * integer, second: duration.second * integer,
microsecond: {ms * integer, p} microsecond: {ms * integer, p}
} }
end end
@@ -339,16 +327,14 @@ defmodule Duration do
""" """
@spec negate(t) :: t @spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do def negate(%Duration{microsecond: {ms, p}} = duration) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{ %Duration{
year: -y, year: -duration.year,
month: -mo, month: -duration.month,
week: -w, week: -duration.week,
day: -d, day: -duration.day,
hour: -h, hour: -duration.hour,
minute: -mi, minute: -duration.minute,
second: -s, second: -duration.second,
microsecond: {-ms, p} microsecond: {-ms, p}
} }
end end
+97 -133
View File
@@ -33,9 +33,8 @@ defmodule Calendar.ISO do
you to format datetimes however else you desire. you to format datetimes however else you desire.
Elixir does not support reduced accuracy formats (for example, a date without Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in components other than seconds the day component) nor decimal precisions in the lowest component (such as
(such as `10:01,5`). Fractional seconds are supported and truncated to `10:01:25,5`).
microsecond precision.
#### Examples #### Examples
@@ -82,11 +81,6 @@ defmodule Calendar.ISO do
iex> Calendar.ISO.parse_time("23") iex> Calendar.ISO.parse_time("23")
{:error, :invalid_format} {:error, :invalid_format}
A decimal fraction is accepted on seconds, but not on any other component:
iex> Calendar.ISO.parse_time("10:01:25,5")
{:ok, {10, 1, 25, {500000, 1}}}
### Extensions ### Extensions
The parser and formatter adopt one ISO 8601 extension: extended year notation. The parser and formatter adopt one ISO 8601 extension: extended year notation.
@@ -423,13 +417,16 @@ defmodule Calendar.ISO do
@spec parse_date(String.t(), format) :: @spec parse_date(String.t(), format) ::
{:ok, {year, month, day}} {:ok, {year, month, day}}
| {:error, atom} | {:error, atom}
def parse_date("-" <> string, format) when is_format(format), def parse_date(string, format) when is_binary(string) and is_format(format),
do: parse_date_guarded(string, format)
defp parse_date_guarded("-" <> string, format),
do: do_parse_date(string, -1, format) do: do_parse_date(string, -1, format)
def parse_date("+" <> string, format) when is_format(format), defp parse_date_guarded("+" <> string, format),
do: do_parse_date(string, 1, format) do: do_parse_date(string, 1, format)
def parse_date(string, format) when is_binary(string) and is_format(format), defp parse_date_guarded(string, format),
do: do_parse_date(string, 1, format) do: do_parse_date(string, 1, format)
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
@@ -505,13 +502,16 @@ defmodule Calendar.ISO do
@spec parse_naive_datetime(String.t(), format) :: @spec parse_naive_datetime(String.t(), format) ::
{:ok, {year, month, day, hour, minute, second, microsecond}} {:ok, {year, month, day, hour, minute, second, microsecond}}
| {:error, atom} | {:error, atom}
def parse_naive_datetime("-" <> string, format) when is_format(format), def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
do: parse_naive_datetime_guarded(string, format)
defp parse_naive_datetime_guarded("-" <> string, format),
do: do_parse_naive_datetime(string, -1, format) do: do_parse_naive_datetime(string, -1, format)
def parse_naive_datetime("+" <> string, format) when is_format(format), defp parse_naive_datetime_guarded("+" <> string, format),
do: do_parse_naive_datetime(string, 1, format) do: do_parse_naive_datetime(string, 1, format)
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format), defp parse_naive_datetime_guarded(string, format),
do: do_parse_naive_datetime(string, 1, format) do: do_parse_naive_datetime(string, 1, format)
defp do_parse_naive_datetime( defp do_parse_naive_datetime(
@@ -606,13 +606,16 @@ defmodule Calendar.ISO do
@spec parse_utc_datetime(String.t(), format) :: @spec parse_utc_datetime(String.t(), format) ::
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset} {:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
| {:error, atom} | {:error, atom}
def parse_utc_datetime("-" <> string, format) when is_format(format), def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
do: parse_utc_datetime_guarded(string, format)
defp parse_utc_datetime_guarded("-" <> string, format),
do: do_parse_utc_datetime(string, -1, format) do: do_parse_utc_datetime(string, -1, format)
def parse_utc_datetime("+" <> string, format) when is_format(format), defp parse_utc_datetime_guarded("+" <> string, format),
do: do_parse_utc_datetime(string, 1, format) do: do_parse_utc_datetime(string, 1, format)
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format), defp parse_utc_datetime_guarded(string, format),
do: do_parse_utc_datetime(string, 1, format) do: do_parse_utc_datetime(string, 1, format)
defp do_parse_utc_datetime( defp do_parse_utc_datetime(
@@ -663,7 +666,7 @@ defmodule Calendar.ISO do
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0}) day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
{{year, month, day}, {hour, minute, second, _}} = {{year, month, day}, {hour, minute, second, _}} =
case add_time_unit_to_iso_days({0, day_fraction}, -offset, :second) do case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
{0, day_fraction} -> {0, day_fraction} ->
{{year, month, day}, time_from_day_fraction(day_fraction)} {{year, month, day}, time_from_day_fraction(day_fraction)}
@@ -680,73 +683,75 @@ defmodule Calendar.ISO do
end end
@doc """ @doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compatible unit pairs. Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`. See `Duration.from_iso8601/1`.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom} @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, 1, [], 0) parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end end
def parse_duration("+P" <> string) when byte_size(string) > 0 do def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, 1, [], 0) parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end end
def parse_duration("-P" <> string) when byte_size(string) > 0 do def parse_duration("-P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, -1, [], 0) with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end end
def parse_duration(_) do def parse_duration(_) do
{:error, :invalid_duration} {:error, :invalid_duration}
end end
defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc} defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, sign, acc, _min_position) defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
when byte_size(string) > 0 do parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
parse_duration_time(string, sign, acc, 0)
end end
defp parse_duration_date(string, sign, acc, min_position) do defp parse_duration_date(string, acc, allowed) do
with {integer, <<unit, rest::binary>>} <- Integer.parse(string), with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, next_min_position} <- find_date_unit(min_position, unit) do {key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position) parse_duration_date(rest, [{key, integer} | acc], allowed)
else else
_ -> {:error, :invalid_date_component} _ -> {:error, :invalid_date_component}
end end
end end
defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc} defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, sign, acc, min_position) do defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] -> {second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
with {:second, _next_min_position} <- find_time_unit(min_position, ?S), case parse_microsecond(rest) do
{{ms, precision}, "S"} <- parse_microsecond(rest) do {{ms, precision}, "S"} ->
ms = ms =
case string do case string do
"-" <> _ -> "-" <> _ ->
-ms -ms
_ -> _ ->
ms ms
end end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
{:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc}
else
_ -> _ ->
{:error, :invalid_time_component} {:error, :invalid_time_component}
end end
{integer, <<unit, rest::binary>>} -> {integer, <<next, rest::binary>>} ->
case find_time_unit(min_position, unit) do case find_unit(allowed, next) do
{key, next_min_position} -> {key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position) false -> {:error, :invalid_time_component}
false ->
{:error, :invalid_time_component}
end end
_ -> _ ->
@@ -754,17 +759,9 @@ defmodule Calendar.ISO do
end end
end end
# The minimum position is the earliest unit still allowed, or one past the end. defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1} defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2} defp find_unit([], _unit), do: false
defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3}
defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4}
defp find_date_unit(_min_position, _unit), do: false
defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1}
defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2}
defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3}
defp find_time_unit(_min_position, _unit), do: false
@doc """ @doc """
Returns the `t:Calendar.iso_days/0` format of the specified date. Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -1137,11 +1134,6 @@ defmodule Calendar.ISO do
It is an integer from 1 to 4. It is an integer from 1 to 4.
In the ISO calendar, the quarter is determined solely by the month, so the `year`
and `day` arguments are ignored. Combination of `year`, `month`, and `day` is not
validated as a valid date, unlike in `day_of_year/3`. Use `valid_date?/3` when
full date validation is required.
## Examples ## Examples
iex> Calendar.ISO.quarter_of_year(2016, 1, 31) iex> Calendar.ISO.quarter_of_year(2016, 1, 31)
@@ -1184,7 +1176,7 @@ defmodule Calendar.ISO do
@doc since: "1.8.0" @doc since: "1.8.0"
@spec year_of_era(year) :: {1..10_000, 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_CE(year), do: {year, 1}
def year_of_era(year) when is_year_BCE(year), do: {1 - year, 0} def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
@doc """ @doc """
Calendar callback to compute the year and era from the Calendar callback to compute the year and era from the
@@ -1237,7 +1229,7 @@ defmodule Calendar.ISO do
end end
def day_of_era(year, month, day) when is_year_BCE(year) do def day_of_era(year, month, day) when is_year_BCE(year) do
day = @iso_epoch - date_to_iso_days(year, month, day) day = abs(date_to_iso_days(year, month, day) - @iso_epoch)
{day, 0} {day, 0}
end end
@@ -1464,7 +1456,7 @@ defmodule Calendar.ISO do
@doc """ @doc """
Converts the given naive_datetime into a iodata. Converts the given naive_datetime into a iodata.
See `naive_datetime_to_string/8` for more information. See `naive_datetime_to_iodata/8` for more information.
## Examples ## Examples
@@ -1589,7 +1581,7 @@ defmodule Calendar.ISO do
@doc """ @doc """
Converts the given datetime into a iodata. Converts the given datetime into a iodata.
See `datetime_to_string/12` for more information. See `datetime_to_iodata/12` for more information.
## Examples ## Examples
@@ -1693,7 +1685,7 @@ defmodule Calendar.ISO do
end end
@doc """ @doc """
Determines if the time given is valid. Determines if the date given is valid according to the proleptic Gregorian calendar.
Leap seconds are not supported by the built-in Calendar.ISO. Leap seconds are not supported by the built-in Calendar.ISO.
@@ -1713,7 +1705,7 @@ defmodule Calendar.ISO do
boolean boolean
def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond) def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond)
when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and
is_integer(ms_precision) do is_integer(ms_value) do
is_hour(hour) and is_minute(minute) and is_second(second) and is_hour(hour) and is_minute(minute) and is_second(second) and
is_microsecond(ms_value, ms_precision) is_microsecond(ms_value, ms_precision)
end end
@@ -1846,9 +1838,6 @@ defmodule Calendar.ISO do
shift_options = shift_datetime_options(duration) shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{:microsecond, {0, _}}, naive_datetime ->
naive_datetime
{_, 0}, naive_datetime -> {_, 0}, naive_datetime ->
naive_datetime naive_datetime
@@ -1878,9 +1867,6 @@ defmodule Calendar.ISO do
shift_options = shift_time_options(duration) shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{:microsecond, {0, _}}, time ->
time
{_, 0}, time -> {_, 0}, time ->
time time
@@ -1943,7 +1929,8 @@ defmodule Calendar.ISO do
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit) def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
add_time_unit_to_iso_days(iso_days, value, unit) ppd = System.convert_time_unit(86_400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end end
defp shift_time_unit_values({0, _}, {_, original_precision}) do defp shift_time_unit_values({0, _}, {_, original_precision}) do
@@ -2024,8 +2011,8 @@ defmodule Calendar.ISO do
total = System.convert_time_unit(integer, unit, :microsecond) total = System.convert_time_unit(integer, unit, :microsecond)
if total in @unix_range_microseconds do if total in @unix_range_microseconds do
{seconds, microseconds} = div_rem(total, @microseconds_per_second) microseconds = Integer.mod(total, @microseconds_per_second)
seconds = @unix_epoch + seconds seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
precision = precision_for_unit(unit) precision = precision_for_unit(unit)
{date, time} = iso_seconds_to_datetime(seconds) {date, time} = iso_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}} {:ok, date, time, {microseconds, precision}}
@@ -2046,27 +2033,38 @@ defmodule Calendar.ISO do
end end
end end
defp parse_microsecond("." <> rest), do: parse_microsecond(rest, rest, 0) defp parse_microsecond("." <> rest) do
defp parse_microsecond("," <> rest), do: parse_microsecond(rest, rest, 0) case parse_microsecond(rest, 0, []) do
defp parse_microsecond(rest), do: {{0, 0}, rest} {[], 0, _} ->
:error
# Digits past the sixth are consumed but do not contribute to the value. {microsecond, precision, rest} ->
defp parse_microsecond(<<head, tail::binary>>, digits, 6) when head in ?0..?9, scale = scale_factor(precision)
do: parse_microsecond(tail, digits, 6) {{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
end
end
defp parse_microsecond(<<head, tail::binary>>, digits, precision) when head in ?0..?9, defp parse_microsecond("," <> rest) do
do: parse_microsecond(tail, digits, precision + 1) parse_microsecond("." <> rest)
end
defp parse_microsecond(_rest, _digits, 0), do: :error defp parse_microsecond(rest) do
{{0, 0}, rest}
end
defp parse_microsecond(rest, digits, precision) do defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
scale = scale_factor(precision) do: parse_microsecond(tail, 6, acc)
microsecond = :erlang.binary_to_integer(:binary.part(digits, 0, precision)) * scale
{{microsecond, precision}, rest} defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
do: parse_microsecond(tail, precision + 1, [head | acc])
defp parse_microsecond(rest, precision, acc) do
{:lists.reverse(acc), precision, rest}
end end
defp parse_offset(""), do: {nil, ""} defp parse_offset(""), do: {nil, ""}
defp parse_offset("Z"), do: {0, ""} defp parse_offset("Z"), do: {0, ""}
defp parse_offset("-00:00"), do: :error
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>), defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
do: parse_offset(1, h1, h2, m1, m2, rest) do: parse_offset(1, h1, h2, m1, m2, rest)
@@ -2089,8 +2087,7 @@ defmodule Calendar.ISO do
true <- m1 in ?0..?5 and m2 in ?0..?9, true <- m1 in ?0..?5 and m2 in ?0..?9,
hour = (h1 - ?0) * 10 + h2 - ?0, hour = (h1 - ?0) * 10 + h2 - ?0,
min = (m1 - ?0) * 10 + m2 - ?0, min = (m1 - ?0) * 10 + m2 - ?0,
true <- hour < 24, true <- hour < 24 do
true <- sign == 1 or hour != 0 or min != 0 do
{(hour * 60 + min) * 60 * sign, rest} {(hour * 60 + min) * 60 * sign, rest}
else else
_ -> :error _ -> :error
@@ -2106,43 +2103,10 @@ defmodule Calendar.ISO do
end end
@doc false @doc false
def iso_days_to_unit(iso_days, :second) do def iso_days_to_unit({days, {parts, ppd}}, unit) do
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), @microseconds_per_second) day_microseconds = days * @parts_per_day
end microseconds = divide_by_parts_per_day(parts, ppd)
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
def iso_days_to_unit(iso_days, :millisecond) do
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), 1_000)
end
def iso_days_to_unit(iso_days, :microsecond) do
iso_days_to_microseconds(iso_days)
end
def iso_days_to_unit(iso_days, unit) do
System.convert_time_unit(iso_days_to_microseconds(iso_days), :microsecond, unit)
end
defp iso_days_to_microseconds({days, {parts, ppd}}) do
days * @parts_per_day + divide_by_parts_per_day(parts, ppd)
end
@doc false
def add_time_unit_to_iso_days(iso_days, add, :second) do
add_day_fraction_to_iso_days(iso_days, add * @microseconds_per_second, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, :millisecond) do
add_day_fraction_to_iso_days(iso_days, add * 1_000, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, :microsecond) do
add_day_fraction_to_iso_days(iso_days, add, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, unit)
when unit == :nanosecond or is_integer(unit) do
ppd = System.convert_time_unit(@seconds_per_day, :second, unit)
add_day_fraction_to_iso_days(iso_days, add, ppd)
end end
@doc false @doc false
+8 -10
View File
@@ -164,7 +164,7 @@ defmodule NaiveDateTime do
Returns the "local time" for the machine the Elixir program is running on. Returns the "local time" for the machine the Elixir program is running on.
WARNING: This function can cause insidious bugs. It depends on the time zone WARNING: This function can cause insidious bugs. It depends on the time zone
configuration at run time. This can change and be set to a time zone that has configuration at run time. This can changed and be set to a time zone that has
daylight saving jumps (spring forward or fall back). daylight saving jumps (spring forward or fall back).
This function can be used to display what the time is right now for the time This function can be used to display what the time is right now for the time
@@ -541,15 +541,15 @@ defmodule NaiveDateTime do
def diff(naive_datetime1, naive_datetime2, unit \\ :second) def diff(naive_datetime1, naive_datetime2, unit \\ :second)
def diff(naive_datetime1, naive_datetime2, :day) do def diff(naive_datetime1, naive_datetime2, :day) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
end end
def diff(naive_datetime1, naive_datetime2, :hour) do def diff(naive_datetime1, naive_datetime2, :hour) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
end end
def diff(naive_datetime1, naive_datetime2, :minute) do def diff(naive_datetime1, naive_datetime2, :minute) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(60)
end end
def diff( def diff(
@@ -570,11 +570,9 @@ defmodule NaiveDateTime do
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" "unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end end
diff_microsecond = units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) - units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) units1 - units2
System.convert_time_unit(diff_microsecond, :microsecond, unit)
end end
@doc """ @doc """
@@ -674,7 +672,7 @@ defmodule NaiveDateTime do
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t() @spec truncate(t(), :microsecond | :millisecond | :second) :: t()
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)} %{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
end end
+18 -41
View File
@@ -62,7 +62,7 @@ defmodule Time do
You can pass a time unit to automatically truncate the resulting time. You can pass a time unit to automatically truncate the resulting time.
The default unit if none gets passed is `:native` which results in a default resolution of microseconds. The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
## Examples ## Examples
@@ -146,9 +146,8 @@ defmodule Time do
iex> Time.new(23, 59, 59, 1_000_000) iex> Time.new(23, 59, 59, 1_000_000)
{:error, :invalid_time} {:error, :invalid_time}
Invalid precision: # Invalid precision
Time.new(23, 59, 59, {999_999, 10})
iex> Time.new(23, 59, 59, {999_999, 10})
{:error, :invalid_time} {:error, :invalid_time}
""" """
@@ -467,12 +466,8 @@ defmodule Time do
Calendar.microsecond(), Calendar.microsecond(),
Calendar.calendar() Calendar.calendar()
) :: t ) :: t
def from_seconds_after_midnight( def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
seconds, when is_integer(seconds) do
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
seconds_in_day = Integer.mod(seconds, @seconds_per_day) seconds_in_day = Integer.mod(seconds, @seconds_per_day)
{hour, minute, second, {_, _}} = {hour, minute, second, {_, _}} =
@@ -483,7 +478,7 @@ defmodule Time do
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, second: second,
microsecond: {microsecond, precision} microsecond: microsecond
} }
end end
@@ -501,7 +496,7 @@ defmodule Time do
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()} @spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
iso_days = {0, to_day_fraction(time)} iso_days = {0, to_day_fraction(time)}
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond} {Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
@@ -694,7 +689,7 @@ defmodule Time do
@doc """ @doc """
Compares two time structs. Compares two time structs.
Returns `:gt` if the first time is later than the second Returns `:gt` if first time is later than the second
and `:lt` for vice versa. If the two times are equal and `:lt` for vice versa. If the two times are equal
`:eq` is returned. `:eq` is returned.
@@ -720,32 +715,14 @@ defmodule Time do
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt @spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
def compare( def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
%{ %{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
hour: hour1, %{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
minute: minute1,
second: second1, case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
microsecond: {microsecond1, _}, {first, second} when first > second -> :gt
calendar: calendar {first, second} when first < second -> :lt
}, _ -> :eq
%{
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, _},
calendar: calendar
}
) do
cond do
hour1 > hour2 -> :gt
hour1 < hour2 -> :lt
minute1 > minute2 -> :gt
minute1 < minute2 -> :lt
second1 > second2 -> :gt
second1 < second2 -> :lt
microsecond1 > microsecond2 -> :gt
microsecond1 < microsecond2 -> :lt
true -> :eq
end end
end end
@@ -924,11 +901,11 @@ defmodule Time do
def diff(time1, time2, unit \\ :second) def diff(time1, time2, unit \\ :second)
def diff(time1, time2, :hour) do def diff(time1, time2, :hour) do
diff(time1, time2, :microsecond) |> div(3_600_000_000) diff(time1, time2, :second) |> div(3600)
end end
def diff(time1, time2, :minute) do def diff(time1, time2, :minute) do
diff(time1, time2, :microsecond) |> div(60_000_000) diff(time1, time2, :second) |> div(60)
end end
def diff( def diff(
+20 -40
View File
@@ -50,7 +50,7 @@ defmodule Code do
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
`ensure_loaded!/1`) to check if a module is loaded before using it and `ensure_loaded!/1`) to check if a module is loaded before using it and
act accordingly. act.
## `ensure_compiled/1` and `ensure_compiled!/1` ## `ensure_compiled/1` and `ensure_compiled!/1`
@@ -258,7 +258,6 @@ defmodule Code do
| {:locals_without_parens, keyword()} | {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()} | {:force_do_end_blocks, boolean()}
| {:migrate, boolean()} | {:migrate, boolean()}
| {:migrate_atom_interpolations, boolean()}
| {:migrate_bitstring_modifiers, boolean()} | {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()} | {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()} | {:migrate_charlists_as_sigils, boolean()}
@@ -273,7 +272,6 @@ defmodule Code do
| {:escape, boolean()} | {:escape, boolean()}
| {:locals_without_parens, keyword()} | {:locals_without_parens, keyword()}
| {:comments, [term()]} | {:comments, [term()]}
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
@typedoc """ @typedoc """
Options for parsing functions that convert strings to quoted expressions. Options for parsing functions that convert strings to quoted expressions.
@@ -287,7 +285,7 @@ defmodule Code do
unescape: boolean(), unescape: boolean(),
existing_atoms_only: boolean(), existing_atoms_only: boolean(),
token_metadata: boolean(), token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}), literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}), static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
emit_warnings: boolean() emit_warnings: boolean()
] ]
@@ -316,7 +314,7 @@ defmodule Code do
:relative_paths :relative_paths
] ]
@list_compiler_options [:tracers, :parser_options, :erlc_options] @list_compiler_options [:tracers, :parser_options]
@available_compiler_options @boolean_compiler_options ++ @available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++ @list_compiler_options ++
@@ -415,7 +413,7 @@ defmodule Code do
operations. operations.
""" """
@spec append_path(Path.t(), cache: boolean()) :: boolean() @spec append_path(Path.t(), cache: boolean()) :: true | false
def append_path(path, opts \\ []) do def append_path(path, opts \\ []) do
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
end end
@@ -554,7 +552,8 @@ defmodule Code do
This is the list of directories the Erlang VM uses for finding This is the list of directories the Erlang VM uses for finding
module code. The list of files is managed per Erlang VM node. module code. The list of files is managed per Erlang VM node.
All paths are expanded with `Path.expand/1` before being deleted. The path is expanded with `Path.expand/1` before being deleted. If the
path does not exist, this function returns `false`.
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec delete_paths([Path.t()]) :: :ok @spec delete_paths([Path.t()]) :: :ok
@@ -768,12 +767,6 @@ defmodule Code do
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options * `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`. to `true` by default. Defaults to `false`.
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`, * `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
removes unnecessary parentheses in known bitstring removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>` [modifiers](`<<>>/1`), for example `<<foo::binary()>>`
@@ -1298,9 +1291,9 @@ defmodule Code do
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST. * `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
It must be a function that receives two arguments, the literal and its It must be a function that receives two arguments, the literal and its
metadata, and it must return `{:ok, ast :: Macro.t}` or metadata, and it must return `{:ok, ast :: Macro.t}` or
`{:error, reason :: binary}`. If you return anything other than the literal `{:error, reason :: binary}`. If you return anything than the literal
itself as the `term`, then the AST is no longer valid. This option itself as the `term`, then the AST is no longer valid. This option
may still be useful for textual analysis of the source code. may still useful for textual analysis of the source code.
* `:static_atoms_encoder` - the static atom encoder function, see * `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this "The `:static_atoms_encoder` function" section below. Note this
@@ -1394,15 +1387,12 @@ defmodule Code do
while preserving information like comments and literals position. while preserving information like comments and literals position.
Returns `{:ok, quoted_form, comments}` if it succeeds, Returns `{:ok, quoted_form, comments}` if it succeeds,
`{:error, {location, error, token}}` otherwise, where `location` `{:error, {line, error, token}}` otherwise.
is keyword metadata containing the line and column of the error.
Comments are maps with the following fields: Comments are maps with the following fields:
* `:line` - The line number of the source code * `:line` - The line number of the source code
* `:column` - The column number of the source code
* `:text` - The full text of the comment, including the leading `#` * `:text` - The full text of the comment, including the leading `#`
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment * `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
@@ -1457,9 +1447,7 @@ defmodule Code do
Returns the AST and a list of comments if it succeeds, raises an exception Returns the AST and a list of comments if it succeeds, raises an exception
otherwise. The exception is a `TokenMissingError` in case a token is missing otherwise. The exception is a `TokenMissingError` in case a token is missing
(usually because the expression is incomplete), `MismatchedDelimiterError` (usually because the expression is incomplete), `SyntaxError` otherwise.
(in case of mismatched opening and closing delimiters) and `SyntaxError`
otherwise.
Check `string_to_quoted/2` for options information. Check `string_to_quoted/2` for options information.
""" """
@@ -1562,13 +1550,9 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from `string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`. escaping the sequences twice. Defaults to `true`.
* `: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 See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all `:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
migration options like `:migrate_charlists_as_sigils`. Note `:line_length` `:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
does not apply here.
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) :: @spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
@@ -1688,7 +1672,7 @@ defmodule Code do
@doc """ @doc """
Stores all given compilation options. Stores all given compilation options.
Changing the compilation options affects all processes Changing the compilation options affect all processes
running in a given Erlang VM node. To store individual running in a given Erlang VM node. To store individual
options and for a description of all options, see options and for a description of all options, see
`put_compiler_option/2`. `put_compiler_option/2`.
@@ -1752,7 +1736,7 @@ defmodule Code do
@doc """ @doc """
Stores a compilation option. Stores a compilation option.
Changing the compilation options affects all processes running in a Changing the compilation options affect all processes running in a
given Erlang VM node. given Erlang VM node.
Available options are: Available options are:
@@ -1768,17 +1752,13 @@ defmodule Code do
remove the `:debug_info` while deploying, tools like `mix release` remove the `:debug_info` while deploying, tools like `mix release`
already do such by default. already do such by default.
Other environments, such as `mix test`, automatically disable this Other environments, such as `mix test`, automatically disables this
via the `:test_elixirc_options` project configuration, as there is via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files. typically no need to store debug chunks for test files.
* `:docs` - when `true`, retains documentation in the compiled module. * `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`. Defaults to `true`.
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
debug metadata for BEAM debuggers. Defaults to `[]`.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn * `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added. when a protocol has already been consolidated and a new implementation is added.
Defaults to `false`. Defaults to `false`.
@@ -1786,13 +1766,13 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has * `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`. already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules * `:infer_signatures` (since v1.18.0) - a list of applications of which modules
should be used during type inference. When `false`, it disables module-local should be using during type inference. When `false`, it disables module-local
signature inference used when type checking remote calls to the compiled signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of the value of this option. module. Type checking will be executed regardless of the value of this option.
Mix projects will set this option to your dependencies list in dev/prod, and Mix projects will set this option to your dependencies list in dev/prod, and
it will disable this option during test (as there is typically no need to infer it will disable this option during test (as there is typically no need to infer
signatures for test files). Outside of Mix projects, it defaults to `[:elixir]`. signature for test files). Outside of Mix projects, it defaults to `[:elixir]`.
* `:module_definition` (since v1.20.0) - stores if the module definition should * `:module_definition` (since v1.20.0) - stores if the module definition should
be `:compiled` (the default) or `:interpreted`. Note this does not affect the be `:compiled` (the default) or `:interpreted`. Note this does not affect the
@@ -1815,7 +1795,7 @@ defmodule Code do
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`. * `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation When `:raise` (the default), undefined variables will trigger a compilation
error. You may set it to `:warn` if you want undefined variables to error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`). of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working This `:warn` behavior only exists for compatibility reasons when working
@@ -2150,7 +2130,7 @@ defmodule Code do
If the module being checked is currently in a compiler deadlock, If the module being checked is currently in a compiler deadlock,
this function returns `{:error, :unavailable}`. Unavailable doesn't this function returns `{:error, :unavailable}`. Unavailable doesn't
necessarily mean the module doesn't exist, just that it is not currently necessarily mean the module doesn't exist, just that it is not currently
available, but it may (or may not) become available in the future. available, but it (or may not) become available in the future.
Therefore, if you can only continue if the module is available, use Therefore, if you can only continue if the module is available, use
`ensure_compiled!/1` instead. In particular, do not do this: `ensure_compiled!/1` instead. In particular, do not do this:
+10 -18
View File
@@ -195,7 +195,6 @@ defmodule Code.Formatter do
file = Keyword.get(opts, :file, nil) file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, []) sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false) migrate = Keyword.get(opts, :migrate, false)
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate) migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate) migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate) migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
@@ -224,7 +223,6 @@ defmodule Code.Formatter do
comments: comments, comments: comments,
sigils: sigils, sigils: sigils,
file: file, file: file,
migrate_atom_interpolations: migrate_atom_interpolations,
migrate_bitstring_modifiers: migrate_bitstring_modifiers, migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe, migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
migrate_charlists_as_sigils: migrate_charlists_as_sigils, migrate_charlists_as_sigils: migrate_charlists_as_sigils,
@@ -336,20 +334,14 @@ defmodule Code.Formatter do
end end
defp quoted_to_algebra( defp quoted_to_algebra(
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} = {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
quoted,
context, context,
state state
) do ) do
cond do if interpolated?(entries) do
not interpolated?(entries) -> interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
remote_to_algebra(quoted, context, state) else
remote_to_algebra(quoted, context, state)
state.migrate_atom_interpolations ->
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
true ->
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
end end
end end
@@ -1465,7 +1457,7 @@ defmodule Code.Formatter do
metadata = [ metadata = [
file: state.file, file: state.file,
line: meta[:line], line: meta[:line],
sigil: String.to_unsafe_atom(name), sigil: String.to_atom(name),
modifiers: modifiers, modifiers: modifiers,
opening_delimiter: opening_delimiter opening_delimiter: opening_delimiter
] ]
@@ -1704,7 +1696,7 @@ defmodule Code.Formatter do
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts) iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
end end
defp integer_to_algebra(text, inspect_opts) do defp integer_to_algebra(text, inspect_otps) do
case text do case text do
<<?0, ?x, rest::binary>> -> <<?0, ?x, rest::binary>> ->
"0x" <> String.upcase(rest) "0x" <> String.upcase(rest)
@@ -1718,15 +1710,15 @@ defmodule Code.Formatter do
decimal -> decimal ->
insert_underscores(decimal) insert_underscores(decimal)
end end
|> color_doc(:number, inspect_opts) |> color_doc(:number, inspect_otps)
end end
defp float_to_algebra(text, inspect_opts) do defp float_to_algebra(text, inspect_otps) do
[int_part, decimal_part] = :binary.split(text, ".") [int_part, decimal_part] = :binary.split(text, ".")
decimal_part = String.downcase(decimal_part) decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_opts) color_doc(string, :number, inspect_otps)
end end
defp insert_underscores("-" <> digits) do defp insert_underscores("-" <> digits) do
+6 -11
View File
@@ -27,8 +27,7 @@ defmodule Code.Fragment do
column: pos_integer(), column: pos_integer(),
columns: boolean(), columns: boolean(),
token_metadata: boolean(), token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}), literal_encoder: (term(), Macro.metadata() -> term()),
preserve_sigils: boolean(),
trailing_fragment: String.t() trailing_fragment: String.t()
] ]
@@ -36,7 +35,7 @@ defmodule Code.Fragment do
Returns the list of lines in the given string, preserving their line endings. Returns the list of lines in the given string, preserving their line endings.
Only the line endings recognized by the Elixir compiler are Only the line endings recognized by the Elixir compiler are
considered, namely `\r\n` and `\n`. If you would like to retrieve considered, namely `\r\n` and `\n`. If you would like the retrieve
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`. lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
## Examples ## Examples
@@ -52,7 +51,6 @@ defmodule Code.Fragment do
""" """
@doc since: "1.19.0" @doc since: "1.19.0"
@spec lines(String.t()) :: [String.t()]
def lines(string) do def lines(string) do
lines(string, <<>>) lines(string, <<>>)
end end
@@ -142,9 +140,6 @@ defmodule Code.Fragment do
* `{:anonymous_call, inside_caller}` - the context is an anonymous * `{:anonymous_call, inside_caller}` - the context is an anonymous
call, such as `fun.(` and `@fun.(`. call, such as `fun.(` and `@fun.(`.
* `{:capture_arg, charlist}` - the context is a capture argument,
such as `&1`
* `{:module_attribute, charlist}` - the context is a module attribute, * `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor` such as `@hello_wor`
@@ -162,8 +157,8 @@ defmodule Code.Fragment do
* `:none` - no context possible * `:none` - no context possible
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning * `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
`~>` and `~>>`, are returned as :operator contexts `~>` and `~>>`
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`. * `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
`inside_struct` can either be a `charlist` in case of a static alias or an `inside_struct` can either be a `charlist` in case of a static alias or an
@@ -662,7 +657,7 @@ defmodule Code.Fragment do
iex> Code.Fragment.surround_context("foo", {1, 1}) iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}} %{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
## Differences from `cursor_context/2` ## Differences to `cursor_context/2`
Because `surround_context/3` attempts to capture complex expressions, Because `surround_context/3` attempts to capture complex expressions,
it has some differences to `cursor_context/2`: it has some differences to `cursor_context/2`:
@@ -676,7 +671,7 @@ defmodule Code.Fragment do
be a local or variable be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}` * `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`) (in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs * This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context `{:struct, ~c""}` as context
+4 -4
View File
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
with "-" <> rest <- Atom.to_string(atom), with "-" <> rest <- Atom.to_string(atom),
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(), [trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
[arity, _inner, _count, ""] <- String.split(trailing, "-") do [arity, _inner, _count, ""] <- String.split(trailing, "-") do
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity} {reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
else else
_ -> :error _ -> :error
end end
@@ -150,20 +150,20 @@ defmodule Code.Identifier do
<<acc::binary, char::utf8>> <<acc::binary, char::utf8>>
end end
defp escape_char(char, acc) when char < 0x80 do defp escape_char(char, acc) when char < 0x100 do
<<a::4, b::4>> = <<char::8>> <<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>> <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
end end
defp escape_char(char, acc) when char < 0x10000 do defp escape_char(char, acc) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>> <<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>> <<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
end end
defp escape_char(char, acc) when char < 0x1000000 do defp escape_char(char, acc) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>> <<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), <<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>> ?}>>
end end
+7 -23
View File
@@ -4,8 +4,6 @@
defmodule Code.Normalizer do defmodule Code.Normalizer do
@moduledoc false @moduledoc false
@do_end_keywords [:rescue, :catch, :else, :after]
defguard is_literal(x) defguard is_literal(x)
when is_integer(x) or when is_integer(x) or
is_float(x) or is_float(x) or
@@ -70,7 +68,7 @@ defmodule Code.Normalizer do
# Bit containers # Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state, state.escape) normalize_bitstring(quoted, state, false)
end end
# Atoms with interpolations # Atoms with interpolations
@@ -184,8 +182,7 @@ defmodule Code.Normalizer do
|> patch_meta_line(state.parent_meta) |> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"") |> Keyword.put_new(:delimiter, "\"")
string = normalize_bitstring(string, %{state | parent_meta: meta}, false) {sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [string, modifiers]}
else else
_ -> _ ->
normalize_call(quoted, state) normalize_call(quoted, state)
@@ -267,7 +264,7 @@ defmodule Code.Normalizer do
"Elixir." <> segments -> "Elixir." <> segments ->
segments segments
|> String.split(".") |> String.split(".")
|> Enum.map(&String.to_unsafe_atom/1) |> Enum.map(&String.to_atom/1)
end end
{:__aliases__, meta, segments} {:__aliases__, meta, segments}
@@ -349,20 +346,18 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta}) args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args} {form, meta, args}
Keyword.has_key?(meta, :do) and kw_blocks?(last) -> Keyword.has_key?(meta, :do) ->
# def foo do :ok end # def foo do :ok end
# def foo, do: :ok # def foo, do: :ok
normalize_kw_blocks(form, meta, args, state) normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and kw_blocks?(last) -> match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
# Non normalized kw blocks # Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line] line = state.parent_meta[:line] || meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]] meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state) normalize_kw_blocks(form, meta, args, state)
true -> true ->
# The formatter renders do-end blocks from the meta alone
meta = Keyword.drop(meta, [:do, :end])
args = normalize_args(args, %{state | parent_meta: meta}) args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, []) {last_arg, leading_args} = List.pop_at(args, -1, [])
@@ -401,22 +396,12 @@ defmodule Code.Normalizer do
defp block_keyword?([]), do: true defp block_keyword?([]), do: true
defp block_keyword?(_), do: false defp block_keyword?(_), do: false
# Anything after the do block that is not a block keyword makes it a keyword list
defp kw_blocks?([{:do, _} | rest] = kw) do
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?(_), do: false
defp allow_keyword?(:when, 2), do: true defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity) defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
parts = maybe_add_trailing_newline(meta, parts, state)
meta = patch_meta_line(meta, state.parent_meta) meta = patch_meta_line(meta, state.parent_meta)
parts = parts =
@@ -435,7 +420,6 @@ defmodule Code.Normalizer do
end) end)
end end
parts = maybe_add_trailing_newline(meta, parts, state)
{:<<>>, meta, parts} {:<<>>, meta, parts}
end end
@@ -575,7 +559,7 @@ defmodule Code.Normalizer do
atom atom
|> Atom.to_string() |> Atom.to_string()
|> maybe_escape_literal(state) |> maybe_escape_literal(state)
|> String.to_unsafe_atom() |> String.to_atom()
end end
defp maybe_escape_literal(term, _) do defp maybe_escape_literal(term, _) do
+9 -11
View File
@@ -80,8 +80,7 @@ defmodule Code.Typespec do
Returns all types available from the module's BEAM code. Returns all types available from the module's BEAM code.
The result is returned as a list of tuples where the first The result is returned as a list of tuples where the first
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+, element is the type (`:typep`, `:type` and `:opaque`).
`:nominal`).
The module must have a corresponding BEAM file which can be The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang located by the runtime system. The types will be in the Erlang
@@ -96,10 +95,9 @@ defmodule Code.Typespec do
types = types =
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code, for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
kind in [:opaque, :type, :nominal] do kind in [:opaque, :type] do
cond do cond do
kind == :opaque -> {:opaque, type} kind == :opaque -> {:opaque, type}
kind == :nominal -> {:nominal, type}
{name, length(args)} in exported_types -> {:type, type} {name, length(args)} in exported_types -> {:type, type}
true -> {:typep, type} true -> {:typep, type}
end end
@@ -119,7 +117,7 @@ defmodule Code.Typespec do
element is spec name and arity and the second is the spec. element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file which can be The module must have a corresponding BEAM file which can be
located by the runtime system. The specs will be in the Erlang located by the runtime system. The types will be in the Erlang
Abstract Format. Abstract Format.
""" """
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error @spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
@@ -137,10 +135,10 @@ defmodule Code.Typespec do
Returns all callbacks available from the module's BEAM code. Returns all callbacks available from the module's BEAM code.
The result is returned as a list of tuples where the first The result is returned as a list of tuples where the first
element is the callback name and arity and the second is the callback. element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file The module must have a corresponding BEAM file
which can be located by the runtime system. The callbacks will be which can be located by the runtime system. The types will be
in the Erlang Abstract Format. in the Erlang Abstract Format.
""" """
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error @spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
@@ -193,8 +191,8 @@ defmodule Code.Typespec do
## To AST conversion ## To AST conversion
defp collect_vars({:ann_type, _anno, [_var, type]}) do defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
collect_vars(type) []
end end
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
@@ -401,10 +399,10 @@ defmodule Code.Typespec do
defp erl_to_ex_var(var) do defp erl_to_ex_var(var) do
case Atom.to_string(var) do case Atom.to_string(var) do
<<"_", c::utf8, rest::binary>> -> <<"_", c::utf8, rest::binary>> ->
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}") String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
<<c::utf8, rest::binary>> -> <<c::utf8, rest::binary>> ->
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}") String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
end end
end end
+3 -6
View File
@@ -152,7 +152,6 @@ defmodule Config do
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec config(atom(), keyword()) :: keyword()
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do if not Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}" raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
@@ -199,7 +198,6 @@ defmodule Config do
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec config(atom(), atom(), term()) :: keyword()
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
get_config!() get_config!()
|> __merge__([{root_key, [{key, opts}]}]) |> __merge__([{root_key, [{key, opts}]}])
@@ -227,7 +225,6 @@ defmodule Config do
""" """
@doc since: "1.18.0" @doc since: "1.18.0"
@spec read_config(atom()) :: keyword() | nil
def read_config(root_key) when is_atom(root_key) do def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key] get_config!()[root_key]
end end
@@ -236,7 +233,7 @@ defmodule Config do
Returns the environment this configuration file is executed on. Returns the environment this configuration file is executed on.
In Mix projects this function returns the environment this configuration In Mix projects this function returns the environment this configuration
file is executed on. file is executed on.
In releases, returns the `MIX_ENV` specified when running `mix release`. In releases, returns the `MIX_ENV` specified when running `mix release`.
This is most often used to execute conditional code: This is most often used to execute conditional code:
@@ -287,8 +284,8 @@ defmodule Config do
In case the file doesn't exist, an error is raised. In case the file doesn't exist, an error is raised.
If the file is relative, it will be expanded relative to the If file is a relative, it will be expanded relatively to the
directory of the current configuration file. directory the current configuration file is in.
## Examples ## Examples
+1 -1
View File
@@ -25,7 +25,7 @@ defmodule Config.Provider do
For example, imagine you want to list some basic configuration For example, imagine you want to list some basic configuration
on Mix's built-in `config/runtime.exs` file, but you also want on Mix's built-in `config/runtime.exs` file, but you also want
to support additional configuration files. To do so, you can add to support additional configuration files. To do so, you can add
this inside the `def project` portion of your `mix.exs`: this inside the `def project` portion of your `mix.exs`:
releases: [ releases: [
demo: [ demo: [
+1 -1
View File
@@ -16,7 +16,7 @@ defmodule Config.Reader do
For example, if you expect the target system to have a config file For example, if you expect the target system to have a config file
in an absolute path, you can add this inside the `def project` portion in an absolute path, you can add this inside the `def project` portion
of your `mix.exs`: of your `mix.exs`:
releases: [ releases: [
demo: [ demo: [
+34 -76
View File
@@ -618,7 +618,7 @@ defmodule Enum do
acc, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), (element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc}) (acc -> {:cont, chunk, acc} | {:cont, acc})
) :: [chunk] ) :: Enumerable.t()
when chunk: any when chunk: any
def chunk_while(enumerable, acc, chunk_fun, after_fun) do def chunk_while(enumerable, acc, chunk_fun, after_fun) do
{_, {res, acc}} = {_, {res, acc}} =
@@ -666,7 +666,7 @@ defmodule Enum do
[1, [2], 3, 4, 5, 6] [1, [2], 3, 4, 5, 6]
""" """
@spec concat(Enumerable.t(Enumerable.t(elem))) :: [elem] when elem: term @spec concat(t) :: t
def concat(enumerables) def concat(enumerables)
def concat(list) when is_list(list) do def concat(list) when is_list(list) do
@@ -681,8 +681,8 @@ defmodule Enum do
Concatenates the enumerable on the `right` with the enumerable on the Concatenates the enumerable on the `right` with the enumerable on the
`left`. `left`.
This function behaves similarly to the `++/2` operator with proper This function produces the same result as the `++/2` operator
lists, but applied to enumerables. for lists.
## Examples ## Examples
@@ -693,7 +693,7 @@ defmodule Enum do
[1, 2, 3, 4, 5, 6] [1, 2, 3, 4, 5, 6]
""" """
@spec concat(Enumerable.t(elem), Enumerable.t(elem)) :: [elem] when elem: term @spec concat(t, t) :: t
def concat(left, right) when is_list(left) and is_list(right) do def concat(left, right) when is_list(left) and is_list(right) do
left ++ right left ++ right
end end
@@ -859,10 +859,6 @@ defmodule Enum do
""" """
@spec dedup_by(t, (element -> term)) :: list @spec dedup_by(t, (element -> term)) :: list
def dedup_by([head | tail], fun) do
dedup_by_list(tail, fun, fun.(head), [head])
end
def dedup_by(enumerable, fun) do def dedup_by(enumerable, fun) do
{list, _} = reduce(enumerable, {[], []}, R.dedup(fun)) {list, _} = reduce(enumerable, {[], []}, R.dedup(fun))
:lists.reverse(list) :lists.reverse(list)
@@ -907,7 +903,7 @@ defmodule Enum do
def drop(enumerable, amount) when is_integer(amount) and amount < 0 do def drop(enumerable, amount) when is_integer(amount) and amount < 0 do
{count, fun} = slice_count_and_fun(enumerable, 1) {count, fun} = slice_count_and_fun(enumerable, 1)
amount = amount + count amount = Kernel.min(amount + count, count)
if amount > 0 do if amount > 0 do
fun.(0, amount, 1) fun.(0, amount, 1)
@@ -1446,7 +1442,7 @@ defmodule Enum do
) )
# Avoid warnings about Dict # Avoid warnings about Dict
dict_module = String.to_unsafe_atom("Dict") dict_module = String.to_atom("Dict")
reduce(reverse(enumerable), dict, fn entry, categories -> reduce(reverse(enumerable), dict, fn entry, categories ->
dict_module.update(categories, fun.(entry), [entry], &[entry | &1]) dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
@@ -2233,19 +2229,14 @@ defmodule Enum do
nil nil
""" """
@spec min_max(t, (element, element -> boolean) | module()) :: {min :: element, max :: element} @spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
@spec min_max(t, (-> empty_result)) :: {min :: element, max :: element} | empty_result @spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
when empty_result: any
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) :: @spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
{min :: element, max :: element} | empty_result {element, element} | empty_result
when empty_result: any when empty_result: any
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end) def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
def min_max(list = [_ | _], empty_fallback) when is_function(empty_fallback, 0) do
min_max_list(list)
end
def min_max(first..last//step = range, empty_fallback) def min_max(first..last//step = range, empty_fallback)
when is_function(empty_fallback, 0) do when is_function(empty_fallback, 0) do
case Range.size(range) do case Range.size(range) do
@@ -2352,14 +2343,14 @@ defmodule Enum do
""" """
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) :: @spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
{min :: element, max :: element} | empty_result {element, element} | empty_result
when empty_result: any when empty_result: any
@spec min_max_by( @spec min_max_by(
t, t,
(element -> any), (element -> any),
(element, element -> boolean) | module(), (element, element -> boolean) | module(),
(-> empty_result) (-> empty_result)
) :: {min :: element, max :: element} | empty_result ) :: {element, element} | empty_result
when empty_result: any when empty_result: any
def min_max_by( def min_max_by(
enumerable, enumerable,
@@ -2403,18 +2394,6 @@ defmodule Enum do
defp min_max_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)
defp min_max_list([h | t]), do: min_max_list(t, h, h)
defp min_max_list([h | t], min, max) do
cond do
h < min -> min_max_list(t, h, max)
max < h -> min_max_list(t, min, h)
true -> min_max_list(t, min, max)
end
end
defp min_max_list([], min, max), do: {min, max}
@doc """ @doc """
Splits the `enumerable` in two lists according to the given function `fun`. Splits the `enumerable` in two lists according to the given function `fun`.
@@ -2692,7 +2671,7 @@ defmodule Enum do
5050 5050
""" """
@spec reduce_while(t, acc, (element, acc -> {:cont, acc} | {:halt, acc})) :: acc @spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
def reduce_while(enumerable, acc, fun) do def reduce_while(enumerable, acc, fun) do
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1) Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
end end
@@ -2935,7 +2914,8 @@ defmodule Enum do
end end
defp slide_list_middle(list, 0, last, start_to_middle) do defp slide_list_middle(list, 0, last, start_to_middle) do
slide_list_last(list, last + 1, [], start_to_middle) {slid_range, tail} = slide_list_last(list, last + 1, [])
slid_range ++ :lists.reverse(start_to_middle, tail)
end end
# You asked for a middle index off the end of the list... you get what we've got # You asked for a middle index off the end of the list... you get what we've got
@@ -2943,16 +2923,16 @@ defmodule Enum do
:lists.reverse(acc) :lists.reverse(acc)
end end
defp slide_list_last([h | t], last, acc, start_to_middle) when last > 0 do defp slide_list_last([h | t], last, acc) when last > 0 do
slide_list_last(t, last - 1, [h | acc], start_to_middle) slide_list_last(t, last - 1, [h | acc])
end end
defp slide_list_last(rest, 0, acc, start_to_middle) do defp slide_list_last(rest, 0, acc) do
:lists.reverse(acc, :lists.reverse(start_to_middle, rest)) {:lists.reverse(acc), rest}
end end
defp slide_list_last([], _, acc, start_to_middle) do defp slide_list_last([], _, acc) do
:lists.reverse(acc, :lists.reverse(start_to_middle)) {:lists.reverse(acc), []}
end end
@doc """ @doc """
@@ -4218,7 +4198,7 @@ defmodule Enum do
`zip_with/2` can be used to transpose lists of lists: `zip_with/2` can be used to transpose lists of lists:
iex> Enum.zip_with([[1, 2], [3, 4]], & &1) iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
[[1, 3], [2, 4]] [[1, 3], [2, 4]]
""" """
@@ -4329,24 +4309,11 @@ defmodule Enum do
empty.() empty.()
_ -> _ ->
# The endpoint shortcut is only valid for sorters consistent with last = last - rem(last - first, step)
# the natural integer order of the range elements, which is known
# to hold for the default sorters; any other sorter traverses the
# elements, seeded with the first one since the range is not empty
if fun == (&<=/2) or fun == (&>=/2) do
last = last - rem(last - first, step)
case fun.(first, last) do case fun.(first, last) do
true -> first true -> first
false -> last false -> last
end
else
reduce_range(first + step, last, step, first, fn element, acc ->
case fun.(acc, element) do
true -> acc
false -> element
end
end)
end end
end end
end end
@@ -4449,9 +4416,10 @@ defmodule Enum do
## any?/2 all?/2 ## any?/2 all?/2
defp predicate_list([h | t], initial, fun) do defp predicate_list([h | t], initial, fun) do
case !!fun.(h) do if !!fun.(h) == initial do
^initial -> predicate_list(t, initial, fun) predicate_list(t, initial, fun)
_ -> not initial else
not initial
end end
end end
@@ -4462,9 +4430,10 @@ defmodule Enum do
defp predicate_range(first, last, step, initial, fun) defp predicate_range(first, last, step, initial, fun)
when step > 0 and first <= last when step > 0 and first <= last
when step < 0 and first >= last do when step < 0 and first >= last do
case !!fun.(first) do if !!fun.(first) == initial do
^initial -> predicate_range(first + step, last, step, initial, fun) predicate_range(first + step, last, step, initial, fun)
_ -> not initial else
not initial
end end
end end
@@ -4547,17 +4516,6 @@ defmodule Enum do
defp dedup_list([value | tail]), do: [value | dedup_list(tail)] defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
defp dedup_list([]), do: [] defp dedup_list([]), do: []
## dedup_by
defp dedup_by_list([head | tail], fun, prev, acc) do
case fun.(head) do
^prev -> dedup_by_list(tail, fun, prev, acc)
new_val -> dedup_by_list(tail, fun, new_val, [head | acc])
end
end
defp dedup_by_list([], _fun, _prev, acc), do: :lists.reverse(acc)
## drop ## drop
defp drop_list(list, 0), do: list defp drop_list(list, 0), do: list
+6 -6
View File
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
iex> raise "oops!" iex> raise "oops!"
** (RuntimeError) oops! ** (RuntimeError) oops!
You should use this exception sparingly, since most of the time it might be You should use this exceptions sparingly, since most of the time it might be
better to define your own exceptions specific to your application or library. better to define your own exceptions specific to your application or library.
Sometimes, however, there are situations in which you don't expect a condition to Sometimes, however, there are situations in which you don't expect a condition to
happen, but you want to give a meaningful error message if it does. In those cases, happen, but you want to give a meaningful error message if it does. In those cases,
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
For example, this can happen if you try to create an atom that is too large: For example, this can happen if you try to create an atom that is too large:
iex> String.to_unsafe_atom(String.duplicate("a", 100_000)) iex> String.to_atom(String.duplicate("a", 100_000))
** (SystemLimitError) a system limit has been reached ** (SystemLimitError) a system limit has been reached
""" """
@@ -1807,7 +1807,7 @@ defmodule UndefinedFunctionError do
defp load_module({name, _path, _loaded?}) do defp load_module({name, _path, _loaded?}) do
name name
|> List.to_unsafe_atom() |> List.to_atom()
|> Code.ensure_loaded() |> Code.ensure_loaded()
end end
@@ -2376,7 +2376,7 @@ defmodule File.Error do
@moduledoc """ @moduledoc """
An exception that is raised when a file operation fails. An exception that is raised when a file operation fails.
For example, this exception is raised, when trying to read a nonexistent file: For example, this exception is raised, when trying to read a non existent file:
iex> File.read!("nonexistent_file.txt") iex> File.read!("nonexistent_file.txt")
** (File.Error) could not read file "nonexistent_file.txt": no such file or directory ** (File.Error) could not read file "nonexistent_file.txt": no such file or directory
@@ -2409,7 +2409,7 @@ defmodule File.CopyError do
@moduledoc """ @moduledoc """
An exception that is raised when copying a file fails. An exception that is raised when copying a file fails.
For example, this exception is raised when trying to copy to a file or directory that isn't present: For example, this exception is raised when trying to copy to file or directory that isn't present:
iex> File.cp_r!("non_existent", "source_dir/subdir") iex> File.cp_r!("non_existent", "source_dir/subdir")
** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory ** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory
@@ -2477,7 +2477,7 @@ defmodule File.LinkError do
@moduledoc """ @moduledoc """
An exception that is raised when linking a file fails. An exception that is raised when linking a file fails.
For example, this exception is raised when trying to link to a file that isn't present: For example, this exception is raised when trying to link to file that isn't present:
iex> File.ln!("existing.txt", "link.txt") iex> File.ln!("existing.txt", "link.txt")
** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory ** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory
+6 -5
View File
@@ -897,7 +897,7 @@ defmodule File do
File.copy("non_existing.txt", "copy.txt") File.copy("non_existing.txt", "copy.txt")
#=> {:error, :enoent} #=> {:error, :enoent}
""" """
@spec copy(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) :: @spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated} {:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
def copy(source, destination, bytes_count \\ :infinity) do def copy(source, destination, bytes_count \\ :infinity) do
source = normalize_path_or_io_device(source) source = normalize_path_or_io_device(source)
@@ -918,7 +918,7 @@ defmodule File do
File.copy!("non_existing.txt", "copy.txt") File.copy!("non_existing.txt", "copy.txt")
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory ** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
""" """
@spec copy!(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) :: @spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
non_neg_integer non_neg_integer
def copy!(source, destination, bytes_count \\ :infinity) do def copy!(source, destination, bytes_count \\ :infinity) do
case copy(source, destination, bytes_count) do case copy(source, destination, bytes_count) do
@@ -1362,8 +1362,9 @@ defmodule File do
end end
defp copy_file_mode(src, dest) do defp copy_file_mode(src, dest) do
with {:ok, src_fileinfo} <- stat(src) do with {:ok, dest_fileinfo} <- stat(dest),
chmod(dest, src_fileinfo.mode) {:ok, src_fileinfo} <- stat(src) do
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
end end
end end
@@ -2220,7 +2221,7 @@ defmodule File do
type. If you pass, for example, `[encoding: :utf8]` or type. If you pass, for example, `[encoding: :utf8]` or
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream `[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
will use `IO.write/2` and the `String.Chars` protocol to convert the data. will use `IO.write/2` and the `String.Chars` protocol to convert the data.
See `IO.binwrite/2` and `IO.write/2`. See `IO.binwrite/2` and `IO.write/2` .
One may also consider passing the `:delayed_write` option if the stream One may also consider passing the `:delayed_write` option if the stream
is meant to be written to under a tight loop. is meant to be written to under a tight loop.
+1 -7
View File
@@ -18,13 +18,7 @@ defmodule File.Stream do
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
@type t :: %__MODULE__{ @type t :: %__MODULE__{}
path: Path.t(),
modes: [term()],
line_or_bytes: :line | pos_integer(),
raw: boolean(),
node: node()
}
@doc false @doc false
def __build__(path, line_or_bytes, modes) do def __build__(path, line_or_bytes, modes) do
+26 -59
View File
@@ -25,7 +25,7 @@ defmodule Float do
and arithmetic due to the fact most decimal fractions cannot be and arithmetic due to the fact most decimal fractions cannot be
represented by a floating-point binary and most operations are not exact, represented by a floating-point binary and most operations are not exact,
but operate on approximations. Those issues are not specific but operate on approximations. Those issues are not specific
to Elixir, they are a property of floating-point representation itself. to Elixir, they are a property of floating point representation itself.
For example, the numbers 0.1 and 0.01 are two of them, what means the result For example, the numbers 0.1 and 0.01 are two of them, what means the result
of squaring 0.1 does not give 0.01 neither the closest representable. Here is of squaring 0.1 does not give 0.01 neither the closest representable. Here is
@@ -167,73 +167,40 @@ defmodule Float do
parse_unsigned(binary) parse_unsigned(binary)
end end
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9, defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_mantissa(binary, rest, false) do: parse_unsigned(rest, false, false, [digit])
defp parse_unsigned(binary) when is_binary(binary), do: :error defp parse_unsigned(binary) when is_binary(binary), do: :error
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9, defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_mantissa(binary, rest, dot?) do: parse_unsigned(rest, dot?, e?, [digit | acc])
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9, defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_mantissa(binary, rest, true) do: parse_unsigned(rest, true, false, [digit, ?. | acc])
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?) defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and digit in ?0..?9, when exp_marker in ~c"eE" and digit in ?0..?9,
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?) defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9, when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?) # :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9, defp parse_unsigned(rest, dot?, _, acc) do
do: parse_exponent(binary, exp_pos, rest, dot?) acc
|> add_dot(dot?)
defp parse_exponent(binary, exp_pos, rest, dot?), |> :lists.reverse()
do: finish_exponent(binary, exp_pos, rest, dot?) |> :erlang.list_to_float()
defp finish_mantissa(binary, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue rescue
ArgumentError -> :error ArgumentError -> :error
else
float -> {float, rest}
end end
# Bare integer: * 1.0 casts to the nearest float without building a new binary, defp add_dot(acc, true), do: acc
# and raises ArithmeticError on overflow (for example a 400-digit integer). defp add_dot(acc, false), do: [?0, ?. | acc]
defp finish_mantissa(binary, rest, _dot? = false) do
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
rescue
ArithmeticError -> :error
end
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
end
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
# form a valid float literal.
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
len = byte_size(binary) - byte_size(rest)
literal =
IO.iodata_to_binary([
:binary.part(binary, 0, exp_pos),
".0",
:binary.part(binary, exp_pos, len - exp_pos)
])
{:erlang.binary_to_float(literal), rest}
rescue
ArgumentError -> :error
end
defp consumed(binary, ""), do: binary
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
@doc """ @doc """
Rounds a float to the largest float less than or equal to `number`. Rounds a float to the largest float less than or equal to `number`.
@@ -286,7 +253,7 @@ defmodule Float do
@doc """ @doc """
Rounds a float to the smallest float greater than or equal to `number`. Rounds a float to the smallest float greater than or equal to `number`.
`ceil/2` also accepts a precision to round a floating-point value up `ceil/2` also accepts a precision to round a floating-point value down
to an arbitrary number of fractional digits (between 0 and 15). to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a The operation is performed on the binary floating point, without a
@@ -355,7 +322,7 @@ defmodule Float do
and therefore the number above is internally represented as 5.567499999, and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals, which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance you must use a decimal library. The behavior above is also in accordance
with reference implementations, such as "Correctly Rounded Binary-Decimal and to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay. Decimal-Binary Conversions" by David M. Gay.
## Examples ## Examples
@@ -379,7 +346,7 @@ defmodule Float do
@spec round(float, precision_range) :: float @spec round(float, precision_range) :: float
def round(float, precision \\ 0) def round(float, precision \\ 0)
def round(float, 0) when float === 0.0 or float === -0.0, do: float def round(float, 0) when float == 0.0, do: float
def round(float, 0) when is_float(float) do def round(float, 0) when is_float(float) do
case :erlang.round(float) * 1.0 do case :erlang.round(float) * 1.0 do
@@ -705,7 +672,7 @@ defmodule Float do
end end
defp invalid_precision_message(precision) do defp invalid_precision_message(precision) do
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}" "precision #{precision} is out of valid range of #{inspect(@precision_range)}"
end end
defp expand_compact([{:compact, false} | t]), do: expand_compact(t) defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
+6 -1
View File
@@ -69,6 +69,7 @@ defmodule Function do
| :name | :name
| :new_index | :new_index
| :new_uniq | :new_uniq
| :pid
| :type | :type
| :uniq | :uniq
@@ -111,6 +112,8 @@ defmodule Function do
When `fun` is an anonymous function (that is, the type is `:local`), the following When `fun` is an anonymous function (that is, the type is `:local`), the following
additional keys are returned: additional keys are returned:
* `:pid` - PID of the process that originally created the function.
* `:index` - (integer) an index into the module function table. * `:index` - (integer) an index into the module function table.
* `:new_index` - (integer) an index into the module function table. * `:new_index` - (integer) an index into the module function table.
@@ -156,7 +159,7 @@ defmodule Function do
`:module`, `:name`, `:arity`, `:env`, or `:type`. `:module`, `:name`, `:arity`, `:env`, or `:type`.
For anonymous functions, there is also information about any of the For anonymous functions, there is also information about any of the
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`. atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
For a named function, the value of any of these items is always the For a named function, the value of any of these items is always the
atom `:undefined`. atom `:undefined`.
@@ -176,6 +179,8 @@ defmodule Function do
iex> fun = &String.length/1 iex> fun = &String.length/1
iex> Function.info(fun, :name) iex> Function.info(fun, :name)
{:name, :length} {:name, :length}
iex> Function.info(fun, :pid)
{:pid, :undefined}
""" """
@doc since: "1.7.0" @doc since: "1.7.0"
+1 -1
View File
@@ -36,7 +36,7 @@ defmodule GenEvent do
alternative. GenStage is an external Elixir library maintained by the Elixir alternative. GenStage is an external Elixir library maintained by the Elixir
team; it provides a tool to implement systems that exchange events in a team; it provides a tool to implement systems that exchange events in a
demand-driven way with built-in support for back-pressure. See the [GenStage demand-driven way with built-in support for back-pressure. See the [GenStage
documentation](https://gen-stage.hexdocs.pm) for more information. documentation](https://hexdocs.pm/gen_stage) for more information.
### `:gen_event` ### `:gen_event`
+1 -1
View File
@@ -1158,7 +1158,7 @@ defmodule GenServer do
## Timeouts ## Timeouts
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds to wait for a reply, or the atom `:infinity` to wait milliseconds to wait for a reply, or the atom `:infinity` to wait
indefinitely. The default value is `5000`. If no reply is received within indefinitely. The default value is `5000`. If no reply is received within
the specified time, the function call fails and the caller exits. If the the specified time, the function call fails and the caller exits. If the
+5 -5
View File
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
module.reduce(dict, acc, fun) module.reduce(dict, acc, fun)
end end
def member?(dict, {key, value}) do def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
{:ok, match?({:ok, ^value}, module.fetch(dict, key))} {:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end end
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do def count(dict) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
{:ok, module.size(dict)} {:ok, module.size(dict)}
end end
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
def into(original) do def into(original) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
collector_fun = fn collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value) dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
def inspect(dict, opts) do def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"]) concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end end
end end
+5 -5
View File
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
module.reduce(set, acc, fun) module.reduce(set, acc, fun)
end end
def member?(set, term) do def member?(set, term) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
{:ok, module.member?(set, term)} {:ok, module.member?(set, term)}
end end
def count(set) do def count(set) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
{:ok, module.size(set)} {:ok, module.size(set)}
end end
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
def into(original) do def into(original) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
collector_fun = fn collector_fun = fn
set, {:cont, term} -> module.put(set, term) set, {:cont, term} -> module.put(set, term)
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
def inspect(set, opts) do def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"]) concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end end
end end
+3 -3
View File
@@ -92,7 +92,7 @@ defprotocol Inspect do
end end
inspect(%Point{x: 1}) inspect(%Point{x: 1})
#=> %Point{x: 1, y: 0} %Point{x: 1, y: 0}
## Custom implementation ## Custom implementation
@@ -436,8 +436,8 @@ defimpl Inspect, for: List do
@doc false @doc false
def keyword?([{key, _value} | rest]) when is_atom(key) do def keyword?([{key, _value} | rest]) when is_atom(key) do
case Atom.to_string(key) do case Atom.to_charlist(key) do
"Elixir." <> _ -> false [?E, ?l, ?i, ?x, ?i, ?r, ?.] ++ _ -> false
_ -> keyword?(rest) _ -> keyword?(rest)
end end
end end
+5 -5
View File
@@ -108,7 +108,7 @@ defmodule Integer do
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0 defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
@doc """ @doc """
Computes `base` raised to the power of `exponent`. Computes `base` raised to power of `exponent`.
Both `base` and `exponent` must be integers. Both `base` and `exponent` must be integers.
The exponent must be zero or positive. The exponent must be zero or positive.
@@ -545,10 +545,10 @@ defmodule Integer do
@doc since: "1.12.0" @doc since: "1.12.0"
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer} @spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
def extended_gcd(0, 0), do: {0, 0, 0} def extended_gcd(0, 0), do: {0, 0, 0}
def extended_gcd(0, b) when is_integer(b) and b > 0, do: {b, 0, 1} def extended_gcd(0, b) when b > 0, do: {b, 0, 1}
def extended_gcd(0, b) when is_integer(b) and b < 0, do: {-b, 0, -1} def extended_gcd(0, b) when b < 0, do: {-b, 0, -1}
def extended_gcd(a, 0) when is_integer(a) and a > 0, do: {a, 1, 0} def extended_gcd(a, 0) when a > 0, do: {a, 1, 0}
def extended_gcd(a, 0) when is_integer(a) and a < 0, do: {-a, -1, 0} def extended_gcd(a, 0) when a < 0, do: {-a, -1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0) extended_gcd(integer2, integer1, 0, 1, 1, 0)
+6 -6
View File
@@ -128,7 +128,7 @@ defmodule IO do
@type nodata :: {:error, term} | :eof @type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | []) @type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, String.Chars.t()}] @type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
@typedoc """ @typedoc """
Stacktrace information as keyword options for `warn/2`. Stacktrace information as keyword options for `warn/2`.
@@ -152,9 +152,9 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument: The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it is the number of Unicode * if `line_or_chars` is an integer, it represents a number of bytes. The device is
code points to be retrieved for devices open in Unicode/utf8 mode. iterated by that number of bytes. This should be the preferred mode for reading
Otherwise, it is the number of raw bytes to be retrieved. non-textual inputs.
* if `line_or_chars` is `:line`, the device is iterated line by line. * if `line_or_chars` is `:line`, the device is iterated line by line.
CRLF newlines ("\r\n") are automatically normalized to "\n". CRLF newlines ("\r\n") are automatically normalized to "\n".
@@ -521,7 +521,7 @@ defmodule IO do
end end
@doc """ @doc """
Gets a number of characters from IO device `:stdio`. Gets a number of bytes from IO device `:stdio`.
If `:stdio` is a Unicode device, `count` implies If `:stdio` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved. the number of Unicode code points to be retrieved.
@@ -549,7 +549,7 @@ defmodule IO do
end end
@doc """ @doc """
Gets a number of characters from the IO `device`. Gets a number of bytes from the IO `device`.
If the IO `device` is a Unicode device, `count` implies If the IO `device` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved. the number of Unicode code points to be retrieved.
+7 -9
View File
@@ -111,15 +111,13 @@ defmodule IO.ANSI do
end end
defsequence = fn name, code, terminator -> defsequence = fn name, code, terminator ->
sequence = "\e[#{code}#{terminator}"
@spec unquote(name)() :: String.t() @spec unquote(name)() :: String.t()
def unquote(name)() do def unquote(name)() do
unquote(sequence) "\e[#{unquote(code)}#{unquote(terminator)}"
end end
defp format_sequence(unquote(name)) do defp format_sequence(unquote(name)) do
unquote(sequence) unquote(name)()
end end
end end
@@ -161,7 +159,7 @@ defmodule IO.ANSI do
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
@doc "Sets alternative font #{font_n}." @doc "Sets alternative font #{font_n}."
defsequence.(String.to_unsafe_atom("font_#{font_n}"), font_n + 10, "m") defsequence.(:"font_#{font_n}", font_n + 10, "m")
end end
@doc "Normal color or intensity." @doc "Normal color or intensity."
@@ -195,13 +193,13 @@ defmodule IO.ANSI do
defsequence.(color, code + 30, "m") defsequence.(color, code + 30, "m")
@doc "Sets foreground color to light #{color}." @doc "Sets foreground color to light #{color}."
defsequence.(String.to_unsafe_atom("light_#{color}"), code + 90, "m") defsequence.(:"light_#{color}", code + 90, "m")
@doc "Sets background color to #{color}." @doc "Sets background color to #{color}."
defsequence.(String.to_unsafe_atom("#{color}_background"), code + 40, "m") defsequence.(:"#{color}_background", code + 40, "m")
@doc "Sets background color to light #{color}." @doc "Sets background color to light #{color}."
defsequence.(String.to_unsafe_atom("light_#{color}_background"), code + 100, "m") defsequence.(:"light_#{color}_background", code + 100, "m")
end end
@doc "Default text color." @doc "Default text color."
@@ -334,7 +332,7 @@ defmodule IO.ANSI do
end end
defp do_format([], [], acc, true, true) do defp do_format([], [], acc, true, true) do
[acc | reset()] [acc | IO.ANSI.reset()]
end end
defp do_format([], [], acc, _emit?, _append_reset) do defp do_format([], [], acc, _emit?, _append_reset) do
+1 -1
View File
@@ -673,7 +673,7 @@ defmodule IO.ANSI.Docs do
# Characters that can mark the beginning or the end of a word. # Characters that can mark the beginning or the end of a word.
# Only support the most common ones at this moment. # Only support the most common ones at this moment.
@delimiters [?\s, ?', ?", ?!, ??, ?,, ?:, ?;, ?/, ?@, ?#, ?$, ?%, ?^, ?&] ++ @delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.] [?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
### Inline start ### Inline start
+1 -1
View File
@@ -31,7 +31,7 @@ defmodule IO.Stream do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
device: IO.device(), device: IO.device(),
raw: boolean(), raw: boolean(),
line_or_bytes: :line | pos_integer() line_or_bytes: :line | non_neg_integer()
} }
@doc false @doc false
+1 -2
View File
@@ -24,7 +24,7 @@ defprotocol JSON.Encoder do
> #### Leaking Private Information {: .error} > #### Leaking Private Information {: .error}
> >
> Prefer using `:only` to avoid accidentally leaking private information when > Prefer using `:only` to avoid accidentally leaking private information when
> new fields are added. Other approaches should be used with caution. > new fields are added. Other approaches should be used with auction.
You can also use `Protocol.derive/3` if you don't own the struct that you want You can also use `Protocol.derive/3` if you don't own the struct that you want
to encode to JSON: to encode to JSON:
@@ -175,7 +175,6 @@ defimpl JSON.Encoder, for: Map do
# Erlang supports only numbers, binaries, and atoms as keys, # Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol. # we support anything that implements the String.Chars protocol.
@compile inline: [key: 2]
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder) defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder) defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder) defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
+12 -17
View File
@@ -142,7 +142,7 @@ defmodule Kernel do
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns, * [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
guards, and extensions guards, and extensions
* [Syntax reference](syntax-reference.md) - the language syntax reference * [Syntax reference](syntax-reference.md) - the language syntax reference
* [Typespecs reference](typespecs.md) - types and function specifications, including list of types * [Typespecs reference](typespecs.md)- types and function specifications, including list of types
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode * [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
## Guards ## Guards
@@ -3088,7 +3088,7 @@ defmodule Kernel do
@doc """ @doc """
Pops a key from the given nested structure. Pops a key from the given nested structure.
Uses the `Access` behaviour to traverse the structures Uses the `Access` protocol to traverse the structures
according to the given `keys`, unless the `key` is a according to the given `keys`, unless the `key` is a
function. If the key is a function, it will be invoked function. If the key is a function, it will be invoked
as specified in `get_and_update_in/3`. as specified in `get_and_update_in/3`.
@@ -4960,7 +4960,6 @@ defmodule Kernel do
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec binary_slice(binary, integer, non_neg_integer) :: binary
def binary_slice(binary, start, size) def binary_slice(binary, start, size)
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
total = byte_size(binary) total = byte_size(binary)
@@ -5034,7 +5033,6 @@ defmodule Kernel do
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec binary_slice(binary, Range.t()) :: binary
def binary_slice(binary, first..last//step) def binary_slice(binary, first..last//step)
when is_binary(binary) and step > 0 do when is_binary(binary) and step > 0 do
total = byte_size(binary) total = byte_size(binary)
@@ -5149,7 +5147,7 @@ defmodule Kernel do
warning saying that a module has been redefined. warning saying that a module has been redefined.
There are some modules that Elixir does not currently implement but it There are some modules that Elixir does not currently implement but it
may implement in the future. Those modules are reserved and defining may be implement in the future. Those modules are reserved and defining
them will result in a compilation error: them will result in a compilation error:
defmodule Any do defmodule Any do
@@ -5250,12 +5248,12 @@ defmodule Kernel do
# defmodule Alias nested # defmodule Alias nested
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
module = :elixir_aliases.concat([env.module, h]) module = :elixir_aliases.concat([env.module, h])
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h)) alias = String.to_atom("Elixir." <> Atom.to_string(h))
opts = [as: alias, warn: false] opts = [as: alias, warn: false]
case t do case t do
[] -> {module, module, opts} [] -> {module, module, opts}
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts} _ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
end end
end end
@@ -5562,7 +5560,7 @@ defmodule Kernel do
when the struct is printed: when the struct is printed:
defmodule User do defmodule User do
@derive {Inspect, only: [:name]} @derive {Inspect, only: :name}
defstruct name: nil, age: nil defstruct name: nil, age: nil
end end
@@ -5589,9 +5587,6 @@ defmodule Kernel do
defstruct name: nil, age: 10 + 11 defstruct name: nil, age: 10 + 11
end end
`@enforce_keys` must be set to an atom or a list of unique atoms,
all of which must name fields defined by `defstruct/1`
Now trying to build a struct without the name key will fail: Now trying to build a struct without the name key will fail:
%User{age: 21} %User{age: 21}
@@ -6283,7 +6278,7 @@ defmodule Kernel do
step through the code it sees). For general stepping, you can set breakpoints step through the code it sees). For general stepping, you can set breakpoints
using `IEx.break!/4`. using `IEx.break!/4`.
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints). For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
## Configuring the debug function ## Configuring the debug function
@@ -6382,7 +6377,7 @@ defmodule Kernel do
### Passing timeouts ### Passing timeouts
You can also pass timeouts directly to this function, that is, milliseconds or You can also pass timeouts directly to this functions, that is, milliseconds or
the atom `:infinity`. In this case, this function just returns the given argument. the atom `:infinity`. In this case, this function just returns the given argument.
## Examples ## Examples
@@ -6902,7 +6897,7 @@ defmodule Kernel do
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string) defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
when alias >= ?A and alias <= ?Z do when alias >= ?A and alias <= ?Z do
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1) string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
{String.to_unsafe_atom("Elixir." <> last_part), string} {String.to_atom("Elixir." <> last_part), string}
end end
defp maybe_atomize_calendar(_last_part, string) do defp maybe_atomize_calendar(_last_part, string) do
@@ -7013,7 +7008,7 @@ defmodule Kernel do
case mod do case mod do
?s -> parts ?s -> parts
?a -> :lists.map(&String.to_unsafe_atom/1, parts) ?a -> :lists.map(&String.to_atom/1, parts)
?c -> :lists.map(&String.to_charlist/1, parts) ?c -> :lists.map(&String.to_charlist/1, parts)
end end
@@ -7022,7 +7017,7 @@ defmodule Kernel do
case mod do case mod do
?s -> parts ?s -> parts
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts))) ?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts))) ?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
end end
end end
@@ -7056,7 +7051,7 @@ defmodule Kernel do
:guard -> :guard ->
raise ArgumentError, raise ArgumentError,
"invalid expression in guard, #{exp} is not allowed in guards. " <> "invalid expression in guard, #{exp} is not allowed in guards. " <>
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html" "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
_ -> _ ->
:ok :ok
+6 -12
View File
@@ -49,7 +49,7 @@ defmodule Kernel.CLI do
@doc """ @doc """
Runs the given function by catching any failure Runs the given function by catching any failure
and printing them to stderr. `at_exit` hooks are and printing them to stdout. `at_exit` hooks are
also invoked before exiting. also invoked before exiting.
This function is used by Elixir's CLI and also This function is used by Elixir's CLI and also
@@ -99,14 +99,9 @@ defmodule Kernel.CLI do
Shared helper for error formatting on CLI tools. Shared helper for error formatting on CLI tools.
""" """
def format_error(kind, reason, stacktrace) do def format_error(kind, reason, stacktrace) do
{banner, rest} = format_error_parts(kind, reason, stacktrace)
[banner, rest]
end
defp format_error_parts(kind, reason, stacktrace) do
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace) {blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
banner = iodata =
case blamed do case blamed do
%FunctionClauseError{} -> %FunctionClauseError{} ->
formatted = Exception.format_banner(kind, reason, stacktrace) formatted = Exception.format_banner(kind, reason, stacktrace)
@@ -117,7 +112,7 @@ defmodule Kernel.CLI do
Exception.format_banner(kind, blamed, stacktrace) Exception.format_banner(kind, blamed, stacktrace)
end end
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]} [iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
end end
@doc """ @doc """
@@ -184,8 +179,7 @@ defmodule Kernel.CLI do
## Error handling ## Error handling
defp print_error(kind, reason, stacktrace) do defp print_error(kind, reason, stacktrace) do
{banner, rest} = format_error_parts(kind, reason, stacktrace) IO.write(:stderr, format_error(kind, reason, stacktrace))
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
end end
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node) defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
@@ -206,7 +200,7 @@ defmodule Kernel.CLI do
end end
@elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++ @elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++
[:elixir_dispatch, :elixir_expand, :elixir_lexical] ++ [:elixir_def, :elixir_dispatch, :elixir_expand, :elixir_lexical] ++
[:elixir_map, :elixir_module] ++ [:elixir_map, :elixir_module] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++ [:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker] [Kernel.ErrorHandler, Module.ParallelChecker]
@@ -433,7 +427,7 @@ defmodule Kernel.CLI do
end end
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
node = List.to_unsafe_atom(node) node = List.to_atom(node)
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined. # Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
_ = :net_kernel.connect_node(node) _ = :net_kernel.connect_node(node)
+3 -3
View File
@@ -23,7 +23,7 @@ defmodule Kernel.LexicalTracker do
@doc """ @doc """
Invoked during module expansion to annotate a require Invoked during module expansion to annotate a require
that must be warned if unused. must be warned if unused.
""" """
def warn_require(pid, meta, module, alias) do def warn_require(pid, meta, module, alias) do
:gen_server.cast(pid, {:warn_require, module, meta, alias}) :gen_server.cast(pid, {:warn_require, module, meta, alias})
@@ -32,7 +32,7 @@ defmodule Kernel.LexicalTracker do
@doc """ @doc """
Invoked during module expansion to annotate an alias Invoked during module expansion to annotate an alias
that must be warned if unused. must be warned if unused.
""" """
def warn_alias(pid, meta, alias, module) do def warn_alias(pid, meta, alias, module) do
:gen_server.cast(pid, {:warn_alias, alias, meta}) :gen_server.cast(pid, {:warn_alias, alias, meta})
@@ -41,7 +41,7 @@ defmodule Kernel.LexicalTracker do
@doc """ @doc """
Invoked during module expansion to annotate an import Invoked during module expansion to annotate an import
that must be warned if unused. must be warned if unused.
""" """
def warn_import(pid, module) do def warn_import(pid, module) do
:gen_server.cast(pid, {:warn_import, module}) :gen_server.cast(pid, {:warn_import, module})
+10 -12
View File
@@ -25,8 +25,8 @@ defmodule Kernel.ParallelCompiler do
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()), each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()), each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()), each_module: (Path.t(), module(), binary() -> term()),
each_cycle: (-> {:compile, [Path.t()], [Code.diagnostic(:warning)]} each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [{module(), Path.t()}], [Code.diagnostic(:warning)]}), | {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(), long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(), long_verification_threshold: pos_integer(),
verification: boolean(), verification: boolean(),
@@ -100,8 +100,6 @@ defmodule Kernel.ParallelCompiler do
This function allows a developer to perform such tasks. This function allows a developer to perform such tasks.
""" """
@doc since: "1.16.0" @doc since: "1.16.0"
@spec pmap(Enumerable.t(input), (input -> output)) :: [output]
when input: term, output: term
def pmap(collection, fun) when is_function(fun, 1) do def pmap(collection, fun) when is_function(fun, 1) do
ref = make_ref() ref = make_ref()
@@ -173,25 +171,25 @@ defmodule Kernel.ParallelCompiler do
* `:each_long_verification` (since v1.19.0) - for each file that takes more * `:each_long_verification` (since v1.19.0) - for each file that takes more
than a given timeout (see the `:long_verification_threshold` option) to than a given timeout (see the `:long_verification_threshold` option) to
verify, invoke this callback passing the module as its argument (and compile, invoke this callback passing the module as its argument (and
optionally the PID of the process verifying the module) optionally the PID of the process verifying the module)
* `:each_module` - for each module compiled, invokes the callback passing * `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode the file, module and the module bytecode
* `:each_cycle` - invoked after each compilation cycle and should return one * `:each_cycle` - after the given files are compiled, invokes this function
of the following values: that should return the following values:
* `{:compile, modules, warnings}` - to continue compilation with a list of * `{:compile, modules, warnings}` - to continue compilation with a list of
further module files to compile further modules to compile
* `{:runtime, modules, warnings}` - to stop compilation and verify the list * `{:runtime, modules, warnings}` - to stop compilation and verify the list
of `{module, path}` pairs because dependent modules have changed of modules because dependent modules have changed
* `:long_compilation_threshold` - the timeout (in seconds) to check for files * `:long_compilation_threshold` - the timeout (in seconds) to check for files
taking too long to compile. For each file that exceeds the threshold, the taking too long to compile. For each file that exceeds the threshold, the
`:each_long_compilation` callback is invoked. Defaults to `10` seconds. `:each_long_compilation` callback is invoked. Defaults to `10` seconds.
* `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to * `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to
check for modules taking too long to verify. For each module that exceeds the check for modules taking too long to compile. For each module that exceeds the
threshold, the `:each_long_verification` callback is invoked. Defaults to threshold, the `:each_long_verification` callback is invoked. Defaults to
`10` seconds. `10` seconds.
@@ -214,7 +212,7 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of * `: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 backward compatibility reasons. This option must be set to true, except for backwards compatibibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel. * `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially. Setting this option to 1 will compile files sequentially.
@@ -271,7 +269,7 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of * `: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 backward compatibility reasons. This option must be set to true, except for backwards compatibibility reasons.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
+7 -7
View File
@@ -768,8 +768,8 @@ defmodule Kernel.SpecialForms do
It is used in typespecs to specify the type of a variable, It is used in typespecs to specify the type of a variable,
function or of a type itself: function or of a type itself:
@type num :: integer | float @type number :: integer | float
@spec add(num, num) :: num @spec add(number, number) :: number
It may also be used in bit strings to specify the type It may also be used in bit strings to specify the type
of a given bit segment: of a given bit segment:
@@ -1333,9 +1333,9 @@ defmodule Kernel.SpecialForms do
sum(1, value, 3) sum(1, value, 3)
end end
However, the code above does not work as expected, because this
injects the representation of the `value` variable, Which the argument for the `:sum` function call is not the
not its contents: expected result:
{:sum, [], [1, {:value, [], Elixir}, 3]} {:sum, [], [1, {:value, [], Elixir}, 3]}
@@ -2077,9 +2077,9 @@ defmodule Kernel.SpecialForms do
iex> try do iex> try do
...> 1 / 0 ...> 1 / 0
...> rescue ...> rescue
...> x in [ArithmeticError] -> {:rescued, Exception.message(x)} ...> x in [ArithmeticError] -> [:rescued, is_exception(x)]
...> end ...> end
{:rescued, "bad argument in arithmetic expression"} [:rescued, true]
Rescue different errors with separate clauses: Rescue different errors with separate clauses:
+10 -21
View File
@@ -686,13 +686,6 @@ defmodule Kernel.Typespec do
{{:type, location(meta), :range, [left, right]}, state} {{:type, location(meta), :range, [left, right]}, state}
end end
defp typespec({:..//, _meta, [_first, _last, _step]} = range, _vars, caller, _state) do
compile_error(
caller,
"ranges with steps are not supported in typespecs, got: #{Macro.to_string(range)}"
)
end
# Handle special forms # Handle special forms
defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do
typespec(caller.module, vars, caller, state) typespec(caller.module, vars, caller, state)
@@ -867,29 +860,29 @@ defmodule Kernel.Typespec do
{{:type, location(meta), :nonempty_string, args}, state} {{:type, location(meta), :nonempty_string, args}, state}
end end
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
if type == :char_list do if type == :char_list do
warning = "the char_list() type is deprecated, use charlist()" warning = "the char_list() type is deprecated, use charlist()"
IO.warn(warning, caller) IO.warn(warning, caller)
end end
remote_typespec(:charlist, meta, [], vars, caller, state) typespec(quote(do: :elixir.charlist()), vars, caller, state)
end end
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state) typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
end end
defp typespec({:struct, meta, []}, vars, caller, state) do defp typespec({:struct, _meta, []}, vars, caller, state) do
remote_typespec(:struct, meta, [], vars, caller, state) typespec(quote(do: :elixir.struct()), vars, caller, state)
end end
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
remote_typespec(:as_boolean, meta, [arg], vars, caller, state) typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
end end
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
remote_typespec(:keyword, meta, args, vars, caller, state) typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
end end
defp typespec({:fun, meta, args}, vars, caller, state) do defp typespec({:fun, meta, args}, vars, caller, state) do
@@ -1014,10 +1007,6 @@ defmodule Kernel.Typespec do
{{:remote_type, location(meta), [remote, name, args]}, state} {{:remote_type, location(meta), [remote, name, args]}, state}
end end
defp remote_typespec(name, meta, args, vars, caller, state) do
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
end
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)] defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
defp collect_union(v), do: [v] defp collect_union(v), do: [v]
+20 -17
View File
@@ -162,8 +162,16 @@ defmodule Kernel.Utils do
# TODO: Make it raise on v2.0 # TODO: Make it raise on v2.0
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env) warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
field_map = :maps.from_list(fields) foreach = fn
struct = :maps.put(:__struct__, module, field_map) key when is_atom(key) ->
:ok
key ->
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false) escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body = body =
@@ -207,7 +215,7 @@ defmodule Kernel.Utils do
end end
end end
case enforce_keys -- :maps.keys(field_map) do case enforce_keys -- :maps.keys(struct) do
[] -> [] ->
mapper = fn {key, val} -> mapper = fn {key, val} ->
%{field: key, default: val, required: :lists.member(key, enforce_keys)} %{field: key, default: val, required: :lists.member(key, enforce_keys)}
@@ -217,9 +225,10 @@ defmodule Kernel.Utils do
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive})) derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body} {struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
invalid_keys -> error_keys ->
raise ArgumentError, raise ArgumentError,
"unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}" "@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
"#{inspect(fields)}"
end end
end end
@@ -320,27 +329,21 @@ defmodule Kernel.Utils do
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t() @spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
def defguard(args, expr, env) do def defguard(args, expr, env) do
{_, vars} = extract_refs_from_args(args) {^args, vars} = extract_refs_from_args(args)
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars) env = :elixir_env.with_vars(%{env | context: :guard}, vars)
body_expr = expand_defguard(expr, %{env | context: nil}, vars) {expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
quote do quote do
case Macro.Env.in_guard?(__CALLER__) do case Macro.Env.in_guard?(__CALLER__) do
true -> true ->
unquote(literal_quote(unquote_every_ref(guard_expr, vars), [])) unquote(literal_quote(unquote_every_ref(expr, vars), []))
false -> false ->
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true)) unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
end end
end end
end end
defp expand_defguard(expr, env, vars) do
env = :elixir_env.with_vars(env, vars)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
expr
end
defp extract_refs_from_args(args) do defp extract_refs_from_args(args) do
Macro.postwalk(args, [], fn Macro.postwalk(args, [], fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) -> {ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -379,7 +382,7 @@ defmodule Kernel.Utils do
{new_var, acc} {new_var, acc}
%{} -> %{} ->
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1)) generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
new_var = Macro.unique_var(generated, module) new_var = Macro.unique_var(generated, module)
{new_var, Map.put(acc, pair, {new_var, var})} {new_var, Map.put(acc, pair, {new_var, var})}
end end
+35 -58
View File
@@ -38,7 +38,7 @@ defmodule Keyword do
## Duplicate keys and ordering ## Duplicate keys and ordering
A keyword list may have duplicate keys so it is not strictly a key-value A keyword may have duplicate keys so it is not strictly a key-value
data type. However, most of the functions in this module work on a data type. However, most of the functions in this module work on a
key-value structure and behave similar to the functions you would key-value structure and behave similar to the functions you would
find in the `Map` module. For example, `Keyword.get/3` will get the first find in the `Map` module. For example, `Keyword.get/3` will get the first
@@ -269,60 +269,38 @@ defmodule Keyword do
@doc since: "1.13.0" @doc since: "1.13.0"
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) :: @spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
{:ok, keyword()} | {:error, [atom]} {:ok, keyword()} | {:error, [atom]}
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
def validate(keyword, values) when is_list(keyword) and is_list(values) do def validate(keyword, values) when is_list(keyword) and is_list(values) do
validate_merge(keyword, values, [], keyword) validate(keyword, values, [], keyword, [])
end end
defp validate_merge([], values, values_pre, original), defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))} case find_key!(key, values1, values2) do
{values1, values2} ->
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original) validate(keyword, values1, values2, original, bad_keys)
when is_atom(key) do
case head do
^key -> validate_merge(keyword, tail, values_pre, original)
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
end
end
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
case find_key!(key, values_pre, []) do
{new_values, new_values_pre} ->
validate_merge(keyword, new_values, new_values_pre, original)
:error -> :error ->
validate_fallback(keyword, values_pre, [key]) case find_key!(key, values2, values1) do
{values1, values2} ->
validate(keyword, values1, values2, original, bad_keys)
:error ->
validate(keyword, values1, values2, original, [key | bad_keys])
end
end end
end end
defp validate_merge([pair | _], _, _, _), defp validate([], values1, values2, original, []) do
do: {:ok, move_pairs!(values1, move_pairs!(values2, original))}
raise(
ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
)
defp validate_fallback([{key, _} | keyword], values, bad_keys)
when is_atom(key) do
case find_key!(key, values, []) do
{rest, acc} ->
validate_fallback(keyword, rest ++ acc, bad_keys)
:error ->
validate_fallback(keyword, values, [key | bad_keys])
end
end end
defp validate_fallback([], _, bad), do: {:error, bad} defp validate([], _values1, _values2, _original, bad_keys) do
{:error, bad_keys}
end
defp validate_fallback([p | _], _, _), defp validate([pair | _], _values1, _values2, _original, []) do
do: raise ArgumentError,
raise( "expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
ArgumentError, end
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
)
defp find_key!(key, [key | rest], acc), do: {rest, acc} defp find_key!(key, [key | rest], acc), do: {rest, acc}
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc} defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
@@ -517,7 +495,7 @@ defmodule Keyword do
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])} {get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop -> :pop ->
{current, :lists.reverse(acc, delete(t, key))} {current, :lists.reverse(acc, t)}
other -> other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}" raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
@@ -583,7 +561,7 @@ defmodule Keyword do
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])} {get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop -> :pop ->
{value, :lists.reverse(acc, delete(t, key))} {value, :lists.reverse(acc, t)}
other -> other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}" raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
@@ -1323,8 +1301,7 @@ defmodule Keyword do
@doc """ @doc """
Drops the given `keys` from the keyword list. Drops the given `keys` from the keyword list.
If a key occurs multiple times, all of its entries are removed when that key Removes duplicate keys from the new keyword list.
is included in `keys`.
## Examples ## Examples
@@ -1365,9 +1342,9 @@ defmodule Keyword do
""" """
@spec pop(t, key, default) :: {value | default, t} @spec pop(t, key, default) :: {value | default, t}
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do case fetch(keywords, key) do
{^key, value} -> {value, delete_key(keywords, key)} {:ok, value} -> {value, delete(keywords, key)}
false -> {default, keywords} :error -> {default, keywords}
end end
end end
@@ -1392,9 +1369,9 @@ defmodule Keyword do
@doc since: "1.10.0" @doc since: "1.10.0"
@spec pop!(t, key) :: {value, t} @spec pop!(t, key) :: {value, t}
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do case fetch(keywords, key) do
{^key, value} -> {value, delete_key(keywords, key)} {:ok, value} -> {value, delete(keywords, key)}
false -> raise KeyError, key: key, term: keywords :error -> raise KeyError, key: key, term: keywords
end end
end end
@@ -1434,7 +1411,7 @@ defmodule Keyword do
do: {values, acc} do: {values, acc}
@doc """ @doc """
Lazily returns the first value for `key` and removes all associated entries in the keyword list. Lazily returns and removes all values associated with `key` in the keyword list.
This is useful if the default value is very expensive to calculate or This is useful if the default value is very expensive to calculate or
generally difficult to set up and tear down again. generally difficult to set up and tear down again.
@@ -1457,9 +1434,9 @@ defmodule Keyword do
@spec pop_lazy(t, key, (-> value)) :: {value, t} @spec pop_lazy(t, key, (-> value)) :: {value, t}
def pop_lazy(keywords, key, fun) def pop_lazy(keywords, key, fun)
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
case :lists.keyfind(key, 1, keywords) do case fetch(keywords, key) do
{^key, value} -> {value, delete_key(keywords, key)} {:ok, value} -> {value, delete(keywords, key)}
false -> {fun.(), keywords} :error -> {fun.(), keywords}
end end
end end
+29 -88
View File
@@ -102,7 +102,7 @@ defmodule List do
Even though the representation changed, the raw data does remain a list of Even though the representation changed, the raw data does remain a list of
integers, which can be handled as such: integers, which can be handled as such:
iex> inspect(~c"abc", charlists: :as_lists) iex> inspect(~c"abc", charlists: :as_list)
"[97, 98, 99]" "[97, 98, 99]"
iex> Enum.map(~c"abc", fn num -> 1000 + num end) iex> Enum.map(~c"abc", fn num -> 1000 + num end)
[1097, 1098, 1099] [1097, 1098, 1099]
@@ -311,6 +311,9 @@ defmodule List do
iex> List.first!([1, 2, 3]) iex> List.first!([1, 2, 3])
1 1
iex> List.first!([])
** (ArgumentError) attempted to get the first element of an empty list
""" """
@doc since: "1.20.0" @doc since: "1.20.0"
@spec first!([elem, ...]) :: elem when elem: var @spec first!([elem, ...]) :: elem when elem: var
@@ -363,6 +366,9 @@ defmodule List do
iex> List.last!([1, 2, 3]) iex> List.last!([1, 2, 3])
3 3
iex> List.last!([])
** (ArgumentError) attempted to get the last element of an empty list
""" """
@doc since: "1.20.0" @doc since: "1.20.0"
@spec last!([elem, ...]) :: elem when elem: var @spec last!([elem, ...]) :: elem when elem: var
@@ -429,7 +435,7 @@ defmodule List do
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec keyfind!([tuple], any, non_neg_integer) :: tuple @spec keyfind!([tuple], any, non_neg_integer) :: any
def keyfind!(list, key, position) when is_integer(position) do def keyfind!(list, key, position) when is_integer(position) do
:lists.keyfind(key, position + 1, list) || :lists.keyfind(key, position + 1, list) ||
raise KeyError, raise KeyError,
@@ -514,8 +520,8 @@ defmodule List do
As in `Enum.sort/2`, avoid using the default sorting function to sort As in `Enum.sort/2`, avoid using the default sorting function to sort
structs, as by default it performs structural comparison instead of a structs, as by default it performs structural comparison instead of a
semantic one. In such cases, you shall pass a sorting function as the third semantic one. In such cases, you shall pass a sorting function as third
argument or any module that implements a `compare/2` function. For example, element or any module that implements a `compare/2` function. For example,
if you have tuples with user names and their birthday, and you want to if you have tuples with user names and their birthday, and you want to
sort on their birthday, in both ascending and descending order, you should sort on their birthday, in both ascending and descending order, you should
do: do:
@@ -655,7 +661,7 @@ defmodule List do
end end
@doc """ @doc """
Wraps `term` in a list if it is not a list. Wraps `term` in a list if this is not list.
If `term` is already a list, it returns the list. If `term` is already a list, it returns the list.
If `term` is `nil`, it returns an empty list. If `term` is `nil`, it returns an empty list.
@@ -954,12 +960,9 @@ defmodule List do
@spec pop_at(list, integer, any) :: {any, list} @spec pop_at(list, integer, any) :: {any, list}
def pop_at(list, index, default \\ nil) when is_integer(index) do def pop_at(list, index, default \\ nil) when is_integer(index) do
if index < 0 do if index < 0 do
case length(list) + index do do_pop_at(list, length(list) + index, default, [])
index when index < 0 -> {default, list}
index -> do_pop_at(list, index, default, [], list)
end
else else
do_pop_at(list, index, default, [], list) do_pop_at(list, index, default, [])
end end
end end
@@ -1021,51 +1024,31 @@ defmodule List do
:lists.suffix(suffix, list) :lists.suffix(suffix, list)
end end
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
def to_atom(charlist) do
:erlang.list_to_atom(charlist)
end
@doc """ @doc """
Converts a charlist to an existing atom or creates a new one. Converts a charlist to an atom.
Elixir supports conversions from charlists which contain any Unicode Elixir supports conversions from charlists which contain any Unicode
code point. code point.
> #### Dynamic Atom Creation {: .warning}
>
> This function creates atoms dynamically and atoms are
> not garbage-collected. Therefore, `charlist` should not be an
> untrusted value, such as input received from a socket or during
> a web request. Consider using `to_existing_atom/1` instead.
By default, the maximum number of atoms is `1_048_576`. This limit
can be raised or lowered using the VM option `+t`.
The maximum atom size is 255 Unicode code points.
Inlined by the compiler. Inlined by the compiler.
## Examples ## Examples
iex> List.to_unsafe_atom(~c"Elixir") iex> List.to_atom(~c"Elixir")
:Elixir :Elixir
iex> List.to_unsafe_atom(~c"🌢 Elixir") iex> List.to_atom(~c"🌢 Elixir")
:"🌢 Elixir" :"🌢 Elixir"
""" """
@doc since: "1.21.0" @spec to_atom(charlist) :: atom
@spec to_unsafe_atom(charlist) :: atom def to_atom(charlist) do
def to_unsafe_atom(charlist) do
:erlang.list_to_atom(charlist) :erlang.list_to_atom(charlist)
end end
@doc """ @doc """
Converts a charlist to an existing atom. Converts a charlist to an existing atom.
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
Elixir supports conversions from charlists which contain any Unicode Elixir supports conversions from charlists which contain any Unicode
code point. Raises an `ArgumentError` if the atom does not exist. code point. Raises an `ArgumentError` if the atom does not exist.
@@ -1096,47 +1079,6 @@ defmodule List do
:erlang.list_to_existing_atom(charlist) :erlang.list_to_existing_atom(charlist)
end end
@doc """
Converts a charlist to one of the `allowed_atoms` or raises.
Raises an `ArgumentError` if the atom either does not exist or is not within
the existing list.
This should be preferred to `to_existing_atom/1` if the list is known upfront,
since there is no risk that the atom has not been loaded.
## Examples
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
:foo
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
"""
@doc since: "1.21.0"
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
atom = :erlang.list_to_existing_atom(charlist)
if atom not in allowed_atoms do
to_existing_atom_unexpected(charlist, allowed_atoms)
end
atom
end
# used just to have a less cryptic stacktrace and consistent error
@doc false
def __to_existing_atom__(charlist, allowed_atoms) do
to_existing_atom_unexpected(charlist, allowed_atoms)
end
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
raise ArgumentError,
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
end
@doc """ @doc """
Returns the float whose text representation is `charlist`. Returns the float whose text representation is `charlist`.
@@ -1148,7 +1090,7 @@ defmodule List do
2.2017764 2.2017764
""" """
@spec to_float(nonempty_charlist) :: float @spec to_float(charlist) :: float
def to_float(charlist) do def to_float(charlist) do
:erlang.list_to_float(charlist) :erlang.list_to_float(charlist)
end end
@@ -1164,7 +1106,7 @@ defmodule List do
123 123
""" """
@spec to_integer(nonempty_charlist) :: integer @spec to_integer(charlist) :: integer
def to_integer(charlist) do def to_integer(charlist) do
:erlang.list_to_integer(charlist) :erlang.list_to_integer(charlist)
end end
@@ -1182,7 +1124,7 @@ defmodule List do
1023 1023
""" """
@spec to_integer(nonempty_charlist, 2..36) :: integer @spec to_integer(charlist, 2..36) :: integer
def to_integer(charlist, base) do def to_integer(charlist, base) do
:erlang.list_to_integer(charlist, base) :erlang.list_to_integer(charlist, base)
end end
@@ -1368,13 +1310,13 @@ defmodule List do
defp myers_difference_with_diff_script(list1, list2, diff_script) do defp myers_difference_with_diff_script(list1, list2, diff_script) do
path = {0, list1, list2, []} path = {0, list1, list2, []}
find_script(0, [path], diff_script) find_script(0, length(list1) + length(list2), [path], diff_script)
end end
defp find_script(envelope, paths, diff_script) do defp find_script(envelope, max, paths, diff_script) do
case each_diagonal(-envelope, envelope, paths, [], diff_script) do case each_diagonal(-envelope, envelope, paths, [], diff_script) do
{:done, edits} -> compact_reverse(edits, []) {:done, edits} -> compact_reverse(edits, [])
{:next, paths} -> find_script(envelope + 1, paths, diff_script) {:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
end end
end end
@@ -1519,16 +1461,15 @@ defmodule List do
# pop_at # pop_at
# The original list is returned when the index is out of bounds defp do_pop_at([], _index, default, acc) do
defp do_pop_at([], _index, default, _acc, original) do {default, :lists.reverse(acc)}
{default, original}
end end
defp do_pop_at([head | tail], 0, _default, acc, _original) do defp do_pop_at([head | tail], 0, _default, acc) do
{head, :lists.reverse(acc, tail)} {head, :lists.reverse(acc, tail)}
end end
defp do_pop_at([head | tail], index, default, acc, original) do defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc], original) do_pop_at(tail, index - 1, default, [head | acc])
end end
end end
+55 -147
View File
@@ -166,7 +166,7 @@ defmodule Macro do
of a `__block__` or the right side of `->`. The last expression of the of a `__block__` or the right side of `->`. The last expression of the
block does not have metadata if it is not followed by an end of line block does not have metadata if it is not followed by an end of line
character (either a newline or `;`). This entry may appear multiple times character (either a newline or `;`). This entry may appear multiple times
in the same metadata if the expression is surrounded by parens in the same metadata if the expression is surround by parens
* `:format` - set to `:keyword` when an atom is defined as a keyword. * `:format` - set to `:keyword` when an atom is defined as a keyword.
It may also be set to `:atom` to distinguish `nil`, `false`, and `true` It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
@@ -235,7 +235,7 @@ defmodule Macro do
`div/2` function, so that the AST for that function will become `{:div, [], `div/2` function, so that the AST for that function will become `{:div, [],
[100, 5]}` (`div(100, 5)`). [100, 5]}` (`div(100, 5)`).
""" """
@spec unpipe(t()) :: [{t(), non_neg_integer}] @spec unpipe(t()) :: [t()]
def unpipe(expr) do def unpipe(expr) do
:lists.reverse(unpipe(expr, [])) :lists.reverse(unpipe(expr, []))
end end
@@ -518,7 +518,7 @@ defmodule Macro do
@doc since: "1.11.3" @doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: [] @spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) :: @spec generate_unique_arguments(pos_integer, context) ::
[{atom, metadata(), context}, ...] [{atom, [counter: integer], context}, ...]
when context: atom when context: atom
def generate_unique_arguments(amount, context), def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2) do: generate_arguments(amount, context, &unique_var/2)
@@ -527,7 +527,7 @@ defmodule Macro do
defp generate_arguments(amount, context, fun) defp generate_arguments(amount, context, fun)
when is_integer(amount) and amount > 0 and is_atom(context) do when is_integer(amount) and amount > 0 and is_atom(context) do
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context) for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
end end
@doc """ @doc """
@@ -576,7 +576,7 @@ defmodule Macro do
""" """
@doc since: "1.11.3" @doc since: "1.11.3"
@spec unique_var(var, context) :: {var, metadata(), context} @spec unique_var(var, context) :: {var, [counter: integer], context}
when var: atom, context: atom when var: atom, context: atom
def unique_var(var, context) when is_atom(var) and is_atom(context) do def unique_var(var, context) when is_atom(var) and is_atom(context) do
{var, [counter: :elixir_module.next_counter(context)], context} {var, [counter: :elixir_module.next_counter(context)], context}
@@ -690,39 +690,7 @@ defmodule Macro do
""" """
@spec prewalk(t, (t -> t)) :: t @spec prewalk(t, (t -> t)) :: t
def prewalk(ast, fun) when is_function(fun, 1) do def prewalk(ast, fun) when is_function(fun, 1) do
do_prewalk(fun.(ast), fun) elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
end
# Mirrors do_traverse/4 with an always-pre fun and no accumulator,
# avoiding the wrapper closures and tuple threading of traverse/4.
# Each clause dispatches on the already-transformed node, so fun's
# rewrites are descended into, exactly as in traverse/4.
defp do_prewalk({form, meta, args}, fun) when is_atom(form) do
{form, meta, do_prewalk_args(args, fun)}
end
defp do_prewalk({form, meta, args}, fun) do
form = do_prewalk(fun.(form), fun)
{form, meta, do_prewalk_args(args, fun)}
end
defp do_prewalk({left, right}, fun) do
left = do_prewalk(fun.(left), fun)
{left, do_prewalk(fun.(right), fun)}
end
defp do_prewalk(list, fun) when is_list(list) do
do_prewalk_args(list, fun)
end
defp do_prewalk(x, _fun) do
x
end
defp do_prewalk_args(args, _fun) when is_atom(args), do: args
defp do_prewalk_args(args, fun) when is_list(args) do
:lists.map(fn x -> do_prewalk(fun.(x), fun) end, args)
end end
@doc """ @doc """
@@ -760,41 +728,11 @@ defmodule Macro do
""" """
@spec postwalk(t, (t -> t)) :: t @spec postwalk(t, (t -> t)) :: t
def postwalk(ast, fun) when is_function(fun, 1) do def postwalk(ast, fun) when is_function(fun, 1) do
do_postwalk(ast, fun) elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
end
# Mirrors do_traverse/4 with an always-post fun and no accumulator,
# avoiding the wrapper closures and tuple threading of traverse/4
defp do_postwalk({form, meta, args}, fun) when is_atom(form) do
fun.({form, meta, do_postwalk_args(args, fun)})
end
defp do_postwalk({form, meta, args}, fun) do
form = do_postwalk(form, fun)
fun.({form, meta, do_postwalk_args(args, fun)})
end
defp do_postwalk({left, right}, fun) do
left = do_postwalk(left, fun)
fun.({left, do_postwalk(right, fun)})
end
defp do_postwalk(list, fun) when is_list(list) do
fun.(do_postwalk_args(list, fun))
end
defp do_postwalk(x, fun) do
fun.(x)
end
defp do_postwalk_args(args, _fun) when is_atom(args), do: args
defp do_postwalk_args(args, fun) when is_list(args) do
:lists.map(fn x -> do_postwalk(x, fun) end, args)
end end
@doc """ @doc """
This function behaves like `prewalk/3`, but performs a depth-first, This functions behaves like `prewalk/3`, but performs a depth-first,
post-order traversal of quoted expressions using an accumulator. post-order traversal of quoted expressions using an accumulator.
""" """
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any} @spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
@@ -1002,7 +940,7 @@ defmodule Macro do
This is useful when a struct needs to be expanded at This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may compilation time and the struct being expanded may or may
not have been compiled (including structs defined not have been compiled (including structs in the defined
under the module being compiled). For compiled modules, under the module being compiled). For compiled modules,
it will invoke `module.__info__(:struct)`. it will invoke `module.__info__(:struct)`.
@@ -1096,7 +1034,7 @@ defmodule Macro do
defp find_invalid(other), do: {:error, other} defp find_invalid(other), do: {:error, other}
@doc """ @doc """
Returns an enumerable that traverses the `ast` in depth-first, Returns an enumerable that traverses the `ast` in depth-first,
pre-order traversal. pre-order traversal.
## Examples ## Examples
@@ -1154,7 +1092,7 @@ defmodule Macro do
end end
@doc """ @doc """
Returns an enumerable that traverses the `ast` in depth-first, Returns an enumerable that traverses the `ast` in depth-first,
post-order traversal. post-order traversal.
## Examples ## Examples
@@ -1257,15 +1195,14 @@ defmodule Macro do
The mapping function receives an integer representing the code point The mapping function receives an integer representing the code point
of the character it wants to unescape. There are also the special atoms of the character it wants to unescape. There are also the special atoms
`:newline`, `:unicode`, and `:hex`, which control newline, unicode, `:newline`, `:unicode`, and `:hex`, which control newline, unicode,
and escaping respectively, and for which the mapping function must return and escaping respectively.
a boolean.
Here is the default mapping function implemented by Elixir: Here is the default mapping function implemented by Elixir:
def unescape_map(:newline), do: true def unescape_map(:newline), do: true
def unescape_map(:unicode), do: true def unescape_map(:unicode), do: true
def unescape_map(:hex), do: true def unescape_map(:hex), do: true
def unescape_map(?0), do: 0 def unescape_map(?0), do: ?0
def unescape_map(?a), do: ?\a def unescape_map(?a), do: ?\a
def unescape_map(?b), do: ?\b def unescape_map(?b), do: ?\b
def unescape_map(?d), do: ?\d def unescape_map(?d), do: ?\d
@@ -1288,11 +1225,7 @@ defmodule Macro do
Macro.unescape_string("example\\n", &unescape_map(&1)) Macro.unescape_string("example\\n", &unescape_map(&1))
""" """
@spec unescape_string( @spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
String.t(),
(non_neg_integer | :newline | :unicode | :hex ->
non_neg_integer | boolean)
) :: String.t()
def unescape_string(string, map) do def unescape_string(string, map) do
:elixir_interpolation.unescape_string(string, map) :elixir_interpolation.unescape_string(string, map)
end end
@@ -1807,9 +1740,9 @@ defmodule Macro do
defp kw_blocks_to_string(kw, fun) do defp kw_blocks_to_string(kw, fun) do
Enum.reduce(unquote(kw_keywords), " ", fn x, acc -> Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
case Keyword.fetch(kw, x) do case Keyword.has_key?(kw, x) do
{:ok, value} -> acc <> kw_block_to_string(x, value, fun) true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
:error -> acc false -> acc
end end
end) <> "end" end) <> "end"
end end
@@ -1923,7 +1856,6 @@ defmodule Macro do
definition compile-time, and you can use `Macro.expand/2`. definition compile-time, and you can use `Macro.expand/2`.
""" """
@doc since: "1.16.0" @doc since: "1.16.0"
@spec compile_apply(module(), atom(), [term()], Macro.Env.t()) :: term()
def compile_apply(mod, fun, args, caller) do def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil}) :elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
Kernel.apply(mod, fun, args) Kernel.apply(mod, fun, args)
@@ -2048,9 +1980,10 @@ defmodule Macro do
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
when is_atom(atom) and is_atom(field) and env.context != :match do when is_atom(atom) and is_atom(field) and env.context != :match do
case Map.fetch(env, field) do if Map.has_key?(env, field) do
{:ok, value} -> {maybe_escape_map(value), true} {maybe_escape_map(Map.get(env, field)), true}
:error -> {original, false} else
{original, false}
end end
end end
@@ -2188,9 +2121,6 @@ defmodule Macro do
def quoted_literal?({:__aliases__, _, args}), def quoted_literal?({:__aliases__, _, args}),
do: quoted_literal?(args) do: quoted_literal?(args)
def quoted_literal?({:__block__, _, [wrapped]}),
do: quoted_literal?(wrapped)
def quoted_literal?({:%, _, [left, right]}), def quoted_literal?({:%, _, [left, right]}),
do: quoted_literal?(left) and quoted_literal?(right) do: quoted_literal?(left) and quoted_literal?(right)
@@ -2570,7 +2500,7 @@ defmodule Macro do
### As a remote call ### As a remote call
Inspect an atom as the function name of a remote call. Inspect an atom the function name of a remote call.
iex> Macro.inspect_atom(:remote_call, :foo) iex> Macro.inspect_atom(:remote_call, :foo)
"foo" "foo"
@@ -2691,76 +2621,54 @@ defmodule Macro do
:unquoted_operator :unquoted_operator
true -> true ->
classify_binary(Atom.to_string(atom), atom) charlist = Atom.to_charlist(atom)
end
end
# ASCII identifiers and aliases are recognized on the binary to avoid building a if valid_alias?(charlist) do
# charlist and running the (unicode aware) tokenizer, which dominates the cost of :alias
# classifying keyword list, map and struct keys. else
defp classify_binary(<<char, rest::binary>>, atom) case :elixir_config.identifier_tokenizer().tokenize(charlist) do
when char >= ?a and char <= ?z {kind, _acc, [], _, _, special} ->
when char == ?_ do cond do
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom) kind != :identifier or :lists.member(:at, special) ->
end :not_callable
defp classify_binary("Elixir" <> rest, atom) do # identifier_tokenizer used to return errors for non-nfc, but
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom) # now it nfc-normalizes everything. However, lack of nfc is
end # still a good reason to quote an atom when printing.
:lists.member(:nfkc, special) ->
:other
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom) true ->
:identifier
end
defp classify_with_tokenizer(atom) do _ ->
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do :other
{kind, _acc, [], _, _, special} -> end
cond do
kind != :identifier or :lists.member(:at, special) ->
:not_callable
# identifier_tokenizer used to return errors for non-nfc, but
# now it nfc-normalizes everything. However, lack of nfc is
# still a good reason to quote an atom when printing.
:lists.member(:nfkc, special) ->
:other
true ->
:identifier
end end
_ ->
:other
end end
end end
defp valid_identifier_rest?(<<char, rest::binary>>) defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
when char >= ?a and char <= ?z defp valid_alias?(_other), do: false
when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9
when char == ?_ do
valid_identifier_rest?(rest)
end
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
defp valid_identifier_rest?(<<>>), do: true do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
defp valid_identifier_rest?(_other), do: false
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z, defp valid_alias_piece?([]), do: true
do: valid_alias_piece_rest?(rest)
defp valid_alias_piece?(<<>>), do: true
defp valid_alias_piece?(_other), do: false defp valid_alias_piece?(_other), do: false
# A helper returning the rest of the binary would build a sub binary per piece, defp trim_leading_while_valid_identifier([char | rest])
# so branch back into valid_alias_piece?/1 to keep the match context.
defp valid_alias_piece_rest?(<<char, rest::binary>>)
when char >= ?a and char <= ?z when char >= ?a and char <= ?z
when char >= ?A and char <= ?Z when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9 when char >= ?0 and char <= ?9
when char == ?_ do when char == ?_ do
valid_alias_piece_rest?(rest) trim_leading_while_valid_identifier(rest)
end end
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other) defp trim_leading_while_valid_identifier(other) do
other
end
@doc """ @doc """
Default backend for `Kernel.dbg/2`. Default backend for `Kernel.dbg/2`.
@@ -2791,7 +2699,7 @@ defmodule Macro do
:guard -> :guard ->
raise ArgumentError, raise ArgumentError,
"invalid expression in guard, dbg is not allowed in guards. " <> "invalid expression in guard, dbg is not allowed in guards. " <>
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html" "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
_ -> _ ->
:ok :ok
@@ -3046,8 +2954,6 @@ defmodule Macro do
@doc false @doc false
def __dbg__(to_debug, header, options) do def __dbg__(to_debug, header, options) do
{print_location?, options} = Keyword.pop(options, :print_location, true) {print_location?, options} = Keyword.pop(options, :print_location, true)
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
ansi_enabled? = options[:syntax_colors] != [] ansi_enabled? = options[:syntax_colors] != []
if print_location? and is_binary(header) do if print_location? and is_binary(header) do
@@ -3055,6 +2961,8 @@ defmodule Macro do
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?)) :ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
end end
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
{formatted, result} = dbg_format_ast_to_debug(to_debug, options) {formatted, result} = dbg_format_ast_to_debug(to_debug, options)
:ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?)) :ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?))
result result
+12 -12
View File
@@ -203,23 +203,23 @@ defmodule Macro.Env do
Returns a keyword list containing the file and line Returns a keyword list containing the file and line
information as keys. information as keys.
""" """
@spec location(t) :: [file: file, line: line] @spec location(t) :: keyword
def location(env) def location(env)
def location(%{__struct__: Macro.Env, file: file, line: line}) do def location(%{__struct__: Macro.Env, file: file, line: line}) do
[file: file, line: line] [file: file, line: line]
end end
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false @doc false
@deprecated "Use Macro.Env.expand_alias/4 instead"
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom), def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}")) do: Keyword.fetch(aliases, :"Elixir.#{atom}")
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false @doc false
@deprecated "Use Macro.Env.expand_alias/4 instead"
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom) def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
when is_atom(atom), when is_atom(atom),
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}")) do: Keyword.fetch(aliases, :"Elixir.#{atom}")
@doc """ @doc """
Returns the modules from which the given `{name, arity}` was Returns the modules from which the given `{name, arity}` was
@@ -234,7 +234,7 @@ defmodule Macro.Env do
> This function does not emit compiler tracing events, > This function does not emit compiler tracing events,
> which may block the compiler from correctly tracking > which may block the compiler from correctly tracking
> dependencies. Use this function for reflection purposes > dependencies. Use this function for reflection purposes
> but do not use it to expand imports into qualified > but to do not use it to expand imports into qualified
> calls. Instead, use `expand_import/5`. > calls. Instead, use `expand_import/5`.
## Examples ## Examples
@@ -345,7 +345,7 @@ defmodule Macro.Env do
## Additional options ## Additional options
It accepts the same options as `Kernel.SpecialForms.require/2` plus: It accepts the same options as `Kernel.SpecialForm.require/2` plus:
* #{trace_option} * #{trace_option}
@@ -386,7 +386,7 @@ defmodule Macro.Env do
## Additional options ## Additional options
It accepts the same options as `Kernel.SpecialForms.import/2` plus: It accepts the same options as `Kernel.SpecialForm.import/2` plus:
* `:emit_warnings` - emit warnings found when defining imports * `:emit_warnings` - emit warnings found when defining imports
@@ -394,7 +394,7 @@ defmodule Macro.Env do
* `:info_callback` - a function to use instead of `c:Module.__info__/1`. * `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument. The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key-value pairs. It has to return a list of `{function, arity}` key value pairs.
If it fails, it defaults to using module metadata based on `module_info/1`. If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples ## Examples
@@ -406,7 +406,7 @@ defmodule Macro.Env do
iex> Macro.Env.lookup_import(env, {:flatten, 1}) iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, List}] [{:function, List}]
It accepts the same options as `Kernel.SpecialForms.import/2`: It accepts the same options as `Kernel.SpecialForm.import/2`:
iex> env = __ENV__ iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:is_odd, 1}) iex> Macro.Env.lookup_import(env, {:is_odd, 1})
@@ -448,7 +448,7 @@ defmodule Macro.Env do
## Additional options ## Additional options
It accepts the same options as `Kernel.SpecialForms.alias/2` plus: It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
* #{trace_option} * #{trace_option}
@@ -700,7 +700,7 @@ defmodule Macro.Env do
@doc """ @doc """
Returns the environment stacktrace. Returns the environment stacktrace.
""" """
@spec stacktrace(t) :: [{module, atom, arity, keyword}] @spec stacktrace(t) :: list
def stacktrace(%{__struct__: Macro.Env} = env) do def stacktrace(%{__struct__: Macro.Env} = env) do
cond do cond do
is_nil(env.module) -> is_nil(env.module) ->
+1 -1
View File
@@ -201,7 +201,7 @@ defmodule Map do
%{} %{}
""" """
@spec new :: %{} @spec new :: map
def new, do: %{} def new, do: %{}
@doc """ @doc """
+17 -22
View File
@@ -51,15 +51,6 @@ defmodule MapSet do
that they share many properties, including logarithmic time complexity. Erlang that they share many properties, including logarithmic time complexity. Erlang
`:sets` (version 2) are implemented on top of maps, so see the documentation `:sets` (version 2) are implemented on top of maps, so see the documentation
for `Map` for more information on its execution time complexity. for `Map` for more information on its execution time complexity.
> #### Dialyzer opaqueness warnings {: .warning}
>
> `MapSet` internally relies on the `:sets` module which uses
> opaque types. This might cause Dialyzer to report opaqueness violations.
> These can be silenced by setting the following module attribute:
>
> @dialyzer :no_opaque
""" """
@type value :: term @type value :: term
@@ -180,14 +171,18 @@ defmodule MapSet do
@doc since: "1.14.0" @doc since: "1.14.0"
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value @spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
map = {small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
if :sets.is_disjoint(set1, set2) do
:sets.union(set1, set2)
else
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
end
%{map_set1 | map: map} disjointer_fun = fn elem, {small, acc} ->
if :sets.is_element(elem, small) do
{:sets.del_element(elem, small), acc}
else
{small, [elem | acc]}
end
end
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
end end
@doc """ @doc """
@@ -210,7 +205,7 @@ defmodule MapSet do
Checks if two sets are equal. Checks if two sets are equal.
The comparison between elements is done using `===/2`, The comparison between elements is done using `===/2`,
which means a set with `1` is not equivalent to a set with which a set with `1` is not equivalent to a set with
`1.0`. `1.0`.
## Examples ## Examples
@@ -349,7 +344,7 @@ defmodule MapSet do
> If you find yourself doing multiple calls to `MapSet.filter/2` > If you find yourself doing multiple calls to `MapSet.filter/2`
> and `MapSet.reject/2` in a pipeline, it is likely more efficient > and `MapSet.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to > to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a set at the end using `MapSet.new/1`. > a map at the end using `MapSet.new/1`.
## Examples ## Examples
@@ -405,15 +400,15 @@ defmodule MapSet do
iex> while_false iex> while_false
MapSet.new([1, 3]) MapSet.new([1, 3])
iex> {while_true, while_false} = MapSet.split_with(MapSet.new([10, 20, 60, 70]), fn v -> v > 50 end) iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end)
iex> while_true iex> while_true
MapSet.new([60, 70]) MapSet.new([])
iex> while_false iex> while_false
MapSet.new([10, 20]) MapSet.new([])
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec split_with(t(a), (a -> as_boolean(term))) :: {t(a), t(a)} when a: value @spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end) {while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
{%MapSet{map: while_true}, %MapSet{map: while_false}} {%MapSet{map: while_true}, %MapSet{map: while_false}}
+10 -12
View File
@@ -190,7 +190,7 @@ defmodule Module do
* a string (often a heredoc) * a string (often a heredoc)
* `false`, which will make the entity invisible to documentation-extraction * `false`, which will make the entity invisible to documentation-extraction
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
* a keyword list, since Elixir 1.7.0 * a keyword list, since Elixir 1.7.0
For example: For example:
@@ -216,7 +216,7 @@ defmodule Module do
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc` As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
also accept a keyword list that serves as a way to provide arbitrary metadata also accept a keyword list that serves as a way to provide arbitrary metadata
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
`IEx` may use this information to display annotations. A common use `IEx` may use this information to display annotations. A common use
case is the `:since` key, which may be used to annotate in which version the case is the `:since` key, which may be used to annotate in which version the
function was introduced. function was introduced.
@@ -263,7 +263,7 @@ defmodule Module do
Tools may use this information to ensure the module is recompiled Tools may use this information to ensure the module is recompiled
in case any of the external resources change, see for example: in case any of the external resources change, see for example:
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html). [`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
The specified file path provided is interpreted as relative to The specified file path provided is interpreted as relative to
the folder containing the project's `mix.exs`, which is the the folder containing the project's `mix.exs`, which is the
@@ -321,7 +321,7 @@ defmodule Module do
Accepts a string (often a heredoc) or `false` where `@moduledoc false` Accepts a string (often a heredoc) or `false` where `@moduledoc false`
will make the module invisible to documentation extraction tools like will make the module invisible to documentation extraction tools like
[`ExDoc`](https://ex-doc.hexdocs.pm/). [`ExDoc`](https://hexdocs.pm/ex_doc/).
Similarly to `@doc` also accepts a keyword list to provide metadata Similarly to `@doc` also accepts a keyword list to provide metadata
about the module. For more details, see the documentation of `@doc` about the module. For more details, see the documentation of `@doc`
@@ -366,8 +366,7 @@ defmodule Module do
Unlike other hooks, `@on_definition` will only invoke functions and Unlike other hooks, `@on_definition` will only invoke functions and
never macros. This is to avoid `@on_definition` callbacks from never macros. This is to avoid `@on_definition` callbacks from
redefining functions that have just been defined in favor of more redefining functions that have just been defined in favor of more
explicit approaches. They are also invoked in the reverse order of explicit approaches.
registration.
When just a module is provided, the function is assumed to be When just a module is provided, the function is assumed to be
`__on_definition__/6`. `__on_definition__/6`.
@@ -754,7 +753,7 @@ defmodule Module do
""" """
@doc since: "1.12.0" @doc since: "1.12.0"
@spec reserved_attributes() :: %{optional(atom()) => %{doc: binary()}} @spec reserved_attributes() :: map
def reserved_attributes() do def reserved_attributes() do
%{ %{
after_compile: %{ after_compile: %{
@@ -1148,7 +1147,7 @@ defmodule Module do
defp simplify_var(var, guess_priority) do defp simplify_var(var, guess_priority) do
case Atom.to_string(var) do case Atom.to_string(var) do
"_" -> {:_, [], guess_priority} "_" -> {:_, [], guess_priority}
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority} "_" <> rest -> {String.to_atom(rest), [], guess_priority}
_ -> {var, [], nil} _ -> {var, [], nil}
end end
end end
@@ -1159,7 +1158,7 @@ defmodule Module do
rescue rescue
ArgumentError -> module ArgumentError -> module
else else
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name))) module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
end end
end end
@@ -1192,7 +1191,7 @@ defmodule Module do
defp expand_key(key, counters) do defp expand_key(key, counters) do
case counters do case counters do
%{^key => count} when is_integer(count) and count >= 1 -> %{^key => count} when is_integer(count) and count >= 1 ->
{{String.to_unsafe_atom("#{key}#{count}"), [], Elixir}, Map.put(counters, key, count - 1)} {{:"#{key}#{count}", [], Elixir}, Map.put(counters, key, count - 1)}
_ -> _ ->
{{key, [], Elixir}, counters} {{key, [], Elixir}, counters}
@@ -1225,8 +1224,7 @@ defmodule Module do
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
# Otherwise, returns a generic guess # Otherwise, returns a generic guess
defp merge_signature({_, meta, _}, _newer, i), defp merge_signature({_, meta, _}, _newer, i), do: {:"arg#{i}", meta, Elixir}
do: {String.to_unsafe_atom("arg#{i}"), meta, Elixir}
@doc """ @doc """
Checks if the module defines the given function or macro. Checks if the module defines the given function or macro.
+5 -1
View File
@@ -228,7 +228,11 @@ defmodule Module.Behaviour do
defp behaviour_defined?(callbacks, behaviour) do defp behaviour_defined?(callbacks, behaviour) do
callbacks callbacks
|> Map.values() |> Map.values()
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end) |> List.flatten()
|> Enum.any?(fn
{_kind, ^behaviour, _optional?} -> true
{_kind, _behaviour, _optional?} -> false
end)
end end
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs) defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
+52 -93
View File
@@ -232,7 +232,7 @@ defmodule Module.Types do
context = context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context -> Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already have the definition # Optimized version of finder, since we already the definition
finder = fn _ -> default_domain(:dynamic, def, 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) {_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context context
@@ -251,23 +251,27 @@ defmodule Module.Types do
context -> context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity) {_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
{used_indexes, unused_indexes} = if pending != [] do
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index}, {used_indexes, unused_indexes} =
{used_indexes, unused_indexes} -> Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
if type_index in pending and not skip_unused_clause?(info, type_index) do {used_indexes, unused_indexes} ->
{used_indexes, [clause_index | unused_indexes]} if type_index in pending and not skip_unused_clause?(info, type_index) do
else {used_indexes, [clause_index | unused_indexes]}
{[clause_index | used_indexes], unused_indexes} else
end {[clause_index | used_indexes], unused_indexes}
end
end)
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end) end)
else
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes context
end
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end end
end end
@@ -383,7 +387,7 @@ defmodule Module.Types do
_ -> _ ->
inferred inferred
|> Enum.map(fn {args, _} -> args end) |> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.opt_union/2) end) |> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
end end
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)} {{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
@@ -394,15 +398,16 @@ defmodule Module.Types do
end end
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
{_, clauses_types, clauses_context} = {_, _, _, domain, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {Pattern.init_previous(), [], context}, fn Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body}, {previous, inferred, acc_context} -> {meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
stack = with_file_meta(stack, meta) stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context) fresh_context = fresh_context(acc_context)
info = {base_info, args, guards} info = {base_info, args, guards}
try do try do
{trees, precise?, head_no_previous_args_types, previous, head_context} = {trees, _precise?, head_no_previous_args_types, previous, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context) Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
{return_type, context} = {return_type, context} =
@@ -410,88 +415,42 @@ defmodule Module.Types do
args_types = Pattern.of_domain(trees, stack, context) args_types = Pattern.of_domain(trees, stack, context)
head_args_types = {type_index, inferred} =
case inferred do add_inferred(inferred, args_types, return_type, total - 1, [])
[] -> nil
_ -> Pattern.of_domain(trees, stack, head_context) domain =
case domain do
[] ->
args_types
_ ->
head_args_types = Pattern.of_domain(trees, stack, head_context)
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
end end
args_triplet = {args_types, head_args_types, head_no_previous_args_types} if type_index == -1 do
inferred = [{args_triplet, return_type, precise?} | inferred] mapping = [{index, total} | mapping]
{previous, inferred, context} {index + 1, total + 1, previous, domain, mapping, inferred, context}
else
mapping = [{index, type_index} | mapping]
{index + 1, total, previous, domain, mapping, inferred, context}
end
rescue rescue
e -> e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack) internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end end
end) end)
{clauses_types, mapping, domain} =
clauses_types
|> Enum.reverse()
|> group_clauses()
domain = domain =
case clauses_types do case clauses_types do
[_] -> nil [_] -> nil
_ -> domain _ -> domain
end end
inferred = {:infer, domain, clauses_types} inferred = {:infer, domain, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(clauses_context, context)} {inferred, mapping, restore_context(clauses_context, context)}
end end
defp group_clauses(clauses) do
{_, all_clauses, filtered_clauses, non_empty?} =
Enum.reduce(clauses, {0, [], [], false}, fn
{_args_triplet, return, precise?} = clause,
{index, all_clauses, filtered_clauses, non_empty?} ->
empty? = Descr.empty?(return)
indexed_clause = {clause, index}
filtered_clauses =
if precise? and empty? do
filtered_clauses
else
[indexed_clause | filtered_clauses]
end
{index + 1, [indexed_clause | all_clauses], filtered_clauses, non_empty? or not empty?}
end)
clauses =
if non_empty? do
Enum.reverse(filtered_clauses)
else
Enum.reverse(all_clauses)
end
[
{{{args, _head_args, _head_no_previous_args}, _return, _precise?}, _index}
| clauses_tail
] = clauses
domain =
Enum.reduce(clauses_tail, args, fn
{{{args, head_args, head_no_previous_args}, _return, _precise?}, _index}, domain ->
compute_domain(args, head_args, head_no_previous_args, domain)
end)
{_, mapping, inferred} =
Enum.reduce(clauses, {0, [], []}, fn
{{{args, _head_args, _head_no_previous_args}, return, _precise?}, index},
{total, mapping, inferred} ->
{type_index, inferred} = add_inferred(inferred, args, return, total - 1, [])
if type_index == -1 do
{total + 1, [{index, total} | mapping], inferred}
else
{total, [{index, type_index} | mapping], inferred}
end
end)
{Enum.reverse(inferred), mapping, domain}
end
defp compute_domain( defp compute_domain(
[arg | args_types], [arg | args_types],
[head_arg | head_args_types], [head_arg | head_args_types],
@@ -506,7 +465,7 @@ defmodule Module.Types do
# the domain is computed by unioning their inferred types. However, their # the domain is computed by unioning their inferred types. However, their
# inferred types often have the different of the previous clauses: # inferred types often have the different of the previous clauses:
# #
# opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1) # union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
# #
# Where `rN` represents the refinement in every function body. # Where `rN` represents the refinement in every function body.
# #
@@ -537,9 +496,9 @@ defmodule Module.Types do
# Furthermore, the signature used in type checking is not refined in any way, # Furthermore, the signature used in type checking is not refined in any way,
# so type checking is still sound. # so type checking is still sound.
if arg == head_arg do if arg == head_arg do
Descr.opt_union(Descr.upper_bound(no_prev_arg), d) Descr.union(Descr.upper_bound(no_prev_arg), d)
else else
Descr.opt_union(arg, d) Descr.union(arg, d)
end end
| compute_domain(args_types, head_args_types, no_prev_args_types, domain) | compute_domain(args_types, head_args_types, no_prev_args_types, domain)
] ]
@@ -550,7 +509,7 @@ defmodule Module.Types do
# We check for term equality of types as an optimization # We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime. # to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc), defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])} do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, index, acc), defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc]) do: add_inferred(tail, args, return, index - 1, [head | acc])
@@ -602,7 +561,7 @@ defmodule Module.Types do
# Allow exactly one differing argument. That one position is widened # Allow exactly one differing argument. That one position is widened
# with union/2. A second difference means the clauses must stay separate. # with union/2. A second difference means the clauses must stay separate.
defp union_args([existing_arg | existing], [arg | args], acc, false) do defp union_args([existing_arg | existing], [arg | args], acc, false) do
union_args(existing, args, [Descr.opt_union(existing_arg, arg) | acc], true) union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
end end
defp union_args([_ | _], [_ | _], _acc, true), do: nil defp union_args([_ | _], [_ | _], _acc, true), do: nil
@@ -672,7 +631,7 @@ defmodule Module.Types do
mode: mode, mode: mode,
# The function for handling local calls # The function for handling local calls
local_handler: handler, local_handler: handler,
# Reverse arrow handling (nil | :cache | :except_none | :include_none) # Reverse arrow handling (nil | :cache | :use)
reverse_arrow: nil reverse_arrow: nil
} }
end end
+111 -209
View File
@@ -33,20 +33,14 @@ defmodule Module.Types.Apply do
|> Enum.map(fn {key, type} when is_atom(key) -> |> Enum.map(fn {key, type} when is_atom(key) ->
tuple([atom([key]), type]) tuple([atom([key]), type])
end) end)
|> Enum.reduce(&opt_union/2) |> Enum.reduce(&union/2)
|> list() |> list()
end end
fas = list(tuple([atom(), integer()])) fas = list(tuple([atom(), integer()]))
struct_info = struct_info =
list( list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
closed_map(
default: {term(), true},
field: {atom(), false},
required: {boolean(), true}
)
)
shared_info = [ shared_info = [
attributes: list(tuple([atom(), list(term())])), attributes: list(tuple([atom(), list(term())])),
@@ -64,7 +58,7 @@ defmodule Module.Types.Apply do
exports_md5: binary(), exports_md5: binary(),
functions: fas, functions: fas,
macros: fas, macros: fas,
struct: struct_info |> opt_union(atom([nil])) struct: struct_info |> union(atom([nil]))
] ++ shared_info ] ++ shared_info
infos = infos =
@@ -80,7 +74,7 @@ defmodule Module.Types.Apply do
module: atom(), module: atom(),
functions: fas, functions: fas,
consolidated?: boolean(), consolidated?: boolean(),
impls: opt_union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))} impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
] ]
for {name, clauses} <- infos do for {name, clauses} <- infos do
@@ -104,10 +98,10 @@ defmodule Module.Types.Apply do
send_destination = send_destination =
pid() pid()
|> opt_union(reference()) |> union(reference())
|> opt_union(port()) |> union(port())
|> opt_union(atom()) |> union(atom())
|> opt_union(tuple([atom(), atom()])) |> union(tuple([atom(), atom()]))
basic_arith_2_args_clauses = [ basic_arith_2_args_clauses = [
{[integer(), integer()], integer()}, {[integer(), integer()], integer()},
@@ -116,24 +110,16 @@ defmodule Module.Types.Apply do
{[float(), float()], float()} {[float(), float()], float()}
] ]
args_or_arity = opt_union(list(term()), integer()) args_or_arity = union(list(term()), integer())
args_or_none = opt_union(list(term()), atom([:none])) args_or_none = union(list(term()), atom([:none]))
custom_info_key = opt_difference(atom(), atom([:file, :line, :error_info])) extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
extra_info =
list(
tuple([atom([:file]), opt_union(list(integer()), binary())])
|> opt_union(tuple([atom([:line]), integer()]))
|> opt_union(tuple([atom([:error_info]), open_map()]))
|> opt_union(tuple([custom_info_key, term()]))
)
raise_stacktrace = raise_stacktrace =
list( list(
tuple([atom(), atom(), args_or_arity, extra_info]) tuple([atom(), atom(), args_or_arity, extra_info])
|> opt_union(tuple([atom(), atom(), args_or_arity])) |> union(tuple([atom(), atom(), args_or_arity]))
|> opt_union(tuple([fun(), args_or_arity, extra_info])) |> union(tuple([fun(), args_or_arity, extra_info]))
|> opt_union(tuple([fun(), args_or_arity])) |> union(tuple([fun(), args_or_arity]))
) )
not_signature = not_signature =
@@ -151,8 +137,6 @@ defmodule Module.Types.Apply do
{[atom([left]), atom([right])], atom([left or right])} {[atom([left]), atom([right])], atom([left or right])}
end end
non_empty_tuple = opt_difference(open_tuple([]), tuple([]))
for {mod, fun, clauses} <- [ for {mod, fun, clauses} <- [
# :binary # :binary
{:binary, :copy, [{[binary(), integer()], binary()}]}, {:binary, :copy, [{[binary(), integer()], binary()}]},
@@ -163,8 +147,7 @@ defmodule Module.Types.Apply do
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]}, {:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :-, basic_arith_2_args_clauses}, {:erlang, :-, basic_arith_2_args_clauses},
{:erlang, :*, basic_arith_2_args_clauses}, {:erlang, :*, basic_arith_2_args_clauses},
{:erlang, :/, {:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
[{[opt_union(integer(), float()), opt_union(integer(), float())], float()}]},
{:erlang, :"/=", [{[term(), term()], boolean()}]}, {:erlang, :"/=", [{[term(), term()], boolean()}]},
{:erlang, :"=/=", [{[term(), term()], boolean()}]}, {:erlang, :"=/=", [{[term(), term()], boolean()}]},
{:erlang, :<, [{[term(), term()], boolean()}]}, {:erlang, :<, [{[term(), term()], boolean()}]},
@@ -194,12 +177,12 @@ defmodule Module.Types.Apply do
{:erlang, :bsr, [{[integer(), integer()], integer()}]}, {:erlang, :bsr, [{[integer(), integer()], integer()}]},
{:erlang, :bxor, [{[integer(), integer()], integer()}]}, {:erlang, :bxor, [{[integer(), integer()], integer()}]},
{:erlang, :byte_size, [{[bitstring()], integer()}]}, {:erlang, :byte_size, [{[bitstring()], integer()}]},
{:erlang, :ceil, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :ceil, [{[union(integer(), float())], integer()}]},
{:erlang, :div, [{[integer(), integer()], integer()}]}, {:erlang, :div, [{[integer(), integer()], integer()}]},
{:erlang, :error, [{[term()], none()}]}, {:erlang, :error, [{[term()], none()}]},
{:erlang, :error, [{[term(), args_or_none], none()}]}, {:erlang, :error, [{[term(), args_or_none], none()}]},
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]}, {:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
{:erlang, :floor, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :floor, [{[union(integer(), float())], integer()}]},
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]}, {:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
{:erlang, :integer_to_binary, [{[integer()], binary()}]}, {:erlang, :integer_to_binary, [{[integer()], binary()}]},
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]}, {:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
@@ -217,12 +200,12 @@ defmodule Module.Types.Apply do
{:erlang, :make_tuple, [{[integer(), term()], tuple()}]}, {:erlang, :make_tuple, [{[integer(), term()], tuple()}]},
{:erlang, :map_size, [{[open_map()], integer()}]}, {:erlang, :map_size, [{[open_map()], integer()}]},
{:erlang, :node, [{[], atom()}]}, {:erlang, :node, [{[], atom()}]},
{:erlang, :node, [{[pid() |> opt_union(reference()) |> opt_union(port())], atom()}]}, {:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
{:erlang, :not, not_signature}, {:erlang, :not, not_signature},
{:erlang, :or, or_signature}, {:erlang, :or, or_signature},
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]}, {:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
{:erlang, :rem, [{[integer(), integer()], integer()}]}, {:erlang, :rem, [{[integer(), integer()], integer()}]},
{:erlang, :round, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :round, [{[union(integer(), float())], integer()}]},
{:erlang, :self, [{[], pid()}]}, {:erlang, :self, [{[], pid()}]},
{:erlang, :spawn, [{[fun(0)], pid()}]}, {:erlang, :spawn, [{[fun(0)], pid()}]},
{:erlang, :spawn, [{mfargs, pid()}]}, {:erlang, :spawn, [{mfargs, pid()}]},
@@ -236,32 +219,33 @@ defmodule Module.Types.Apply do
{[bitstring_no_binary(), integer()], tuple([binary(), bitstring_no_binary()])} {[bitstring_no_binary(), integer()], tuple([binary(), bitstring_no_binary()])}
]}, ]},
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]}, {:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
{:erlang, :trunc, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :trunc, [{[union(integer(), float())], integer()}]},
# TODO: Replace term()/dynamic() by parametric types
{:erlang, :++, {:erlang, :++,
[ [
{[empty_list(), term()], dynamic(term())}, {[empty_list(), term()], dynamic(term())},
{[non_empty_list(term()), term()], dynamic(non_empty_list(term(), term()))} {[non_empty_list(term()), term()], dynamic(non_empty_list(term(), term()))}
]}, ]},
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]}, {:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
{:erlang, :delete_element, [{[integer(), non_empty_tuple], dynamic(open_tuple([]))}]}, {:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},
{:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]}, {:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :element, [{[integer(), non_empty_tuple], dynamic()}]}, {:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
{:erlang, :insert_element, {:erlang, :insert_element,
[{[integer(), open_tuple([]), term()], dynamic(non_empty_tuple)}]}, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]}, {:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
{:erlang, :map_get, [{[term(), open_map()], term()}]}, {:erlang, :map_get, [{[term(), open_map()], term()}]},
{:erlang, :max, [{[term(), term()], dynamic()}]}, {:erlang, :max, [{[term(), term()], dynamic()}]},
{:erlang, :min, [{[term(), term()], dynamic()}]}, {:erlang, :min, [{[term(), term()], dynamic()}]},
{:erlang, :send, [{[send_destination, term()], dynamic()}]}, {:erlang, :send, [{[send_destination, term()], dynamic()}]},
{:erlang, :setelement, {:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
[{[integer(), non_empty_tuple, term()], dynamic(non_empty_tuple)}]},
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]}, {:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]}, {:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
## Kernel ## Kernel
{Kernel, :elem, [{[non_empty_tuple, integer()], dynamic()}]}, {Kernel, :elem, [{[open_tuple([]), integer()], dynamic()}]},
{Kernel, :is_map_key, [{[open_map(), term()], boolean()}]}, {Kernel, :is_map_key, [{[open_map(), term()], boolean()}]},
{Kernel, :put_elem, [{[non_empty_tuple, integer(), term()], dynamic(non_empty_tuple)}]}, {Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
## Lists ## Lists
{:lists, :member, {:lists, :member,
@@ -273,10 +257,9 @@ defmodule Module.Types.Apply do
## Map ## Map
{Map, :delete, [{[open_map(), term()], open_map()}]}, {Map, :delete, [{[open_map(), term()], open_map()}]},
{Map, :fetch, {Map, :fetch,
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]}, [{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
{Map, :fetch!, [{[open_map(), term()], term()}]}, {Map, :fetch!, [{[open_map(), term()], term()}]},
{Map, :from_struct, {Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
[{[open_map(__struct__: {atom(), false})], open_map(__struct__: {none(), true})}]},
{Map, :get, [{[open_map(), term()], term()}]}, {Map, :get, [{[open_map(), term()], term()}]},
{Map, :get, [{[open_map(), term(), term()], term()}]}, {Map, :get, [{[open_map(), term(), term()], term()}]},
{Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]}, {Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]},
@@ -293,24 +276,22 @@ defmodule Module.Types.Apply do
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]}, {Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
{Map, :update, [{[open_map(), term(), term(), fun(1)], open_map()}]}, {Map, :update, [{[open_map(), term(), term(), fun(1)], open_map()}]},
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]}, {Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
{Tuple, :delete_at, [{[non_empty_tuple, integer()], dynamic(open_tuple([]))}]}, {Tuple, :delete_at, [{[open_tuple([]), integer()], dynamic(open_tuple([]))}]},
{Tuple, :duplicate, [{[term(), integer()], tuple()}]}, {Tuple, :duplicate, [{[term(), integer()], tuple()}]},
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(non_empty_tuple)}]}, {Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
{:maps, :from_keys, [{[list(term()), term()], open_map()}]}, {:maps, :from_keys, [{[list(term()), term()], open_map()}]},
{:maps, :find, {:maps, :find,
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]}, [{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
{:maps, :get, [{[term(), open_map()], term()}]}, {:maps, :get, [{[term(), open_map()], term()}]},
{:maps, :is_key, [{[term(), open_map()], boolean()}]}, {:maps, :is_key, [{[term(), open_map()], boolean()}]},
{:maps, :keys, [{[open_map()], list(term())}]}, {:maps, :keys, [{[open_map()], list(term())}]},
{:maps, :put, [{[term(), term(), open_map()], open_map()}]}, {:maps, :put, [{[term(), term(), open_map()], open_map()}]},
{:maps, :remove, [{[term(), open_map()], open_map()}]}, {:maps, :remove, [{[term(), open_map()], open_map()}]},
{:maps, :take, {:maps, :take,
[{[term(), open_map()], tuple([term(), open_map()]) |> opt_union(atom([:error]))}]}, [{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
{:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]}, {:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]},
{:maps, :update, [{[term(), term(), open_map()], open_map()}]}, {:maps, :update, [{[term(), term(), open_map()], open_map()}]},
{:maps, :values, [{[open_map()], list(term())}]}, {:maps, :values, [{[open_map()], list(term())}]}
{String, :to_existing_atom, [{[binary(), non_empty_list(atom())], atom()}]},
{List, :to_existing_atom, [{[list(integer()), non_empty_list(atom())], atom()}]}
] do ] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq() [arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
@@ -323,7 +304,7 @@ defmodule Module.Types.Apply do
domain = domain =
clauses clauses
|> Enum.map(fn {args, _} -> args end) |> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &opt_union/2) end) |> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
{:strong, domain, clauses} {:strong, domain, clauses}
end end
@@ -340,9 +321,9 @@ defmodule Module.Types.Apply do
is_float: float(), is_float: float(),
is_function: fun(), is_function: fun(),
is_integer: integer(), is_integer: integer(),
is_list: opt_union(empty_list(), non_empty_list(term(), term())), is_list: union(empty_list(), non_empty_list(term(), term())),
is_map: open_map(), is_map: open_map(),
is_number: opt_union(float(), integer()), is_number: union(float(), integer()),
is_pid: pid(), is_pid: pid(),
is_port: port(), is_port: port(),
is_reference: reference(), is_reference: reference(),
@@ -354,7 +335,7 @@ defmodule Module.Types.Apply do
{:strong, [term()], {:strong, [term()],
[ [
{[type], atom([true])}, {[type], atom([true])},
{[Module.Types.Descr.opt_negation(type)], atom([false])} {[negation(type)], atom([false])}
]} ]}
def signature(:erlang, unquote(guard), 1), def signature(:erlang, unquote(guard), 1),
@@ -368,7 +349,7 @@ defmodule Module.Types.Apply do
{:strong, [term(), integer(), integer()], {:strong, [term(), integer(), integer()],
[ [
{[integer(), integer(), integer()], boolean()}, {[integer(), integer(), integer()], boolean()},
{[Module.Types.Descr.opt_negation(integer()), integer(), integer()], atom([false])} {[negation(integer()), integer(), integer()], atom([false])}
]} ]}
) )
) )
@@ -466,14 +447,9 @@ defmodule Module.Types.Apply do
# a and not number(), b and not number() -> a and b # a and not number(), b and not number() -> a and b
# #
# However, during inference, we type it as `a, b -> a and b` only. # However, during inference, we type it as `a, b -> a and b` only.
# Either argument may be discarded based on term ordering, so the result's {left_type, context} = of_fun.(left, expected, expr, stack, context)
# expected type cannot be used to refine either argument. The result itself {right_type, context} = of_fun.(right, expected, expr, stack, context)
# flows directly to the consumer, so it can still be contextually refined. result = union(left_type, right_type)
{left_type, context} = of_fun.(left, term(), expr, stack, context)
{right_type, context} = of_fun.(right, term(), expr, stack, context)
result = opt_union(left_type, right_type)
refined_result = opt_intersection(result, expected)
result = if empty?(refined_result), do: result, else: refined_result
if error = mismatched_ordered_comparison(left_type, right_type, stack) do if error = mismatched_ordered_comparison(left_type, right_type, stack) do
remote_error(error, :erlang, name, 2, expr, stack, context) remote_error(error, :erlang, name, 2, expr, stack, context)
@@ -698,14 +674,10 @@ defmodule Module.Types.Apply do
{type, context} = of_fun.(literal, term(), expr, stack, context) {type, context} = of_fun.(literal, term(), expr, stack, context)
if singleton?(type) do if singleton?(type) do
acc = acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
if polarity,
do: opt_union(acc, type),
else: opt_intersection(acc, Module.Types.Descr.opt_negation(type))
{acc, all_singleton?, context} {acc, all_singleton?, context}
else else
acc = if polarity, do: opt_union(acc, type), else: acc acc = if polarity, do: union(acc, type), else: acc
{acc, false, context} {acc, false, context}
end end
end) end)
@@ -743,9 +715,9 @@ defmodule Module.Types.Apply do
@empty_list empty_list() @empty_list empty_list()
@non_empty_list non_empty_list(term()) @non_empty_list non_empty_list(term())
@list opt_union(empty_list(), non_empty_list(term())) @list union(empty_list(), non_empty_list(term()))
@empty_map empty_map() @empty_map empty_map()
@non_empty_map opt_difference(open_map(), empty_map()) @non_empty_map difference(open_map(), empty_map())
# Limit the size of tuples to 16 entries # Limit the size of tuples to 16 entries
# as otherwise we may create large nodes # as otherwise we may create large nodes
@@ -805,7 +777,7 @@ defmodule Module.Types.Apply do
{tuple(List.duplicate(term(), literal)), true} {tuple(List.duplicate(term(), literal)), true}
:tuple_size -> :tuple_size ->
{opt_difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true} {difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
end end
{actual, context} = of_fun.(arg, expected, expr, stack, context) {actual, context} = of_fun.(arg, expected, expr, stack, context)
@@ -836,7 +808,7 @@ defmodule Module.Types.Apply do
# This logic mirrors the code in `Pattern.of_pattern_tree` # This logic mirrors the code in `Pattern.of_pattern_tree`
# If it is a singleton, we can always be precise # If it is a singleton, we can always be precise
if singleton?(type) do if singleton?(type) do
expected = if polarity, do: type, else: Module.Types.Descr.opt_negation(type) expected = if polarity, do: type, else: negation(type)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context) {arg_type, context} = of_fun.(arg, expected, expr, stack, context)
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean() result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
@@ -944,13 +916,13 @@ defmodule Module.Types.Apply do
defp expected_order(_, :<, 0), do: :none defp expected_order(_, :<, 0), do: :none
defp expected_order(:tuple_size, :<, size), defp expected_order(:tuple_size, :<, size),
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true} do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
defp expected_order(:tuple_size, :"=<", 0), defp expected_order(:tuple_size, :"=<", 0),
do: {tuple([]), true} do: {tuple([]), true}
defp expected_order(:tuple_size, :"=<", size), defp expected_order(:tuple_size, :"=<", size),
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true} do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
defp expected_order(:tuple_size, :>, size), defp expected_order(:tuple_size, :>, size),
do: {open_tuple(List.duplicate(term(), size + 1)), true} do: {open_tuple(List.duplicate(term(), size + 1)), true}
@@ -1006,7 +978,7 @@ defmodule Module.Types.Apply do
{:strong, [term(), integer()], {:strong, [term(), integer()],
[ [
{[type, integer()], atom([true])}, {[type, integer()], atom([true])},
{[Module.Types.Descr.opt_negation(type), integer()], atom([false])} {[negation(type), integer()], atom([false])}
]} ]}
{info, filter_domain(info, expected, 2), context} {info, filter_domain(info, expected, 2), context}
@@ -1017,71 +989,61 @@ defmodule Module.Types.Apply do
info = info =
{:strong, [term(), open_map()], {:strong, [term(), open_map()],
[ [
{[term(), open_map([{key, {term(), false}}])], atom([true])}, {[term(), open_map([{key, term()}])], atom([true])},
{[term(), open_map([{key, {none(), true}}])], atom([false])} {[term(), open_map([{key, not_set()}])], atom([false])}
]} ]}
{info, filter_domain(info, expected, 2), context} {info, filter_domain(info, expected, 2), context}
end end
def remote_domain(:maps, :is_key, [key, map], expected, meta, stack, context)
when is_atom(key) do
remote_domain(:erlang, :is_map_key, [key, map], expected, meta, stack, context)
end
def remote_domain(Kernel, :is_map_key, [_map, key], expected, _meta, _stack, context) def remote_domain(Kernel, :is_map_key, [_map, key], expected, _meta, _stack, context)
when is_atom(key) do when is_atom(key) do
info = info =
{:strong, [open_map(), term()], {:strong, [open_map(), term()],
[ [
{[open_map([{key, {term(), false}}]), term()], atom([true])}, {[open_map([{key, term()}]), term()], atom([true])},
{[open_map([{key, {none(), true}}]), term()], atom([false])} {[open_map([{key, not_set()}]), term()], atom([false])}
]} ]}
{info, filter_domain(info, expected, 2), context} {info, filter_domain(info, expected, 2), context}
end end
def remote_domain(Map, :has_key?, [map, key], expected, meta, stack, context)
when is_atom(key) do
remote_domain(Kernel, :is_map_key, [map, key], expected, meta, stack, context)
end
def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context) def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context)
when is_atom(key) do when is_atom(key) do
domain = [term(), open_map([{key, {expected, false}}])] domain = [term(), open_map([{key, expected}])]
{{:strong, nil, [{domain, term()}]}, domain, context} {{:strong, nil, [{domain, term()}]}, domain, context}
end end
def remote_domain(Map, :fetch!, [_, key], expected, _meta, _stack, context) when is_atom(key) do def remote_domain(Map, :fetch!, [_, key], expected, _meta, _stack, context) when is_atom(key) do
domain = [open_map([{key, {expected, false}}]), term()] domain = [open_map([{key, expected}]), term()]
{{:strong, nil, [{domain, term()}]}, domain, context} {{:strong, nil, [{domain, term()}]}, domain, context}
end end
def remote_domain(:maps, :get, [key, _], expected, _meta, _stack, context) when is_atom(key) do def remote_domain(:maps, :get, [key, _], expected, _meta, _stack, context) when is_atom(key) do
domain = [term(), open_map([{key, {expected, false}}])] domain = [term(), open_map([{key, expected}])]
{{:strong, nil, [{domain, term()}]}, domain, context} {{:strong, nil, [{domain, term()}]}, domain, context}
end end
def remote_domain(Map, :replace!, [_, key, _], _expected, _meta, _stack, context) def remote_domain(Map, :replace!, [_, key, _], _expected, _meta, _stack, context)
when is_atom(key) do when is_atom(key) do
domain = [open_map([{key, {term(), false}}]), term(), term()] domain = [open_map([{key, term()}]), term(), term()]
{{:strong, nil, [{domain, open_map()}]}, domain, context} {{:strong, nil, [{domain, open_map()}]}, domain, context}
end end
def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context) def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context)
when is_atom(key) do when is_atom(key) do
domain = [term(), term(), open_map([{key, {term(), false}}])] domain = [term(), term(), open_map([{key, term()}])]
{{:strong, nil, [{domain, open_map()}]}, domain, context} {{:strong, nil, [{domain, open_map()}]}, domain, context}
end end
def remote_domain(Map, :pop!, [_, key], _expected, _meta, _stack, context) when is_atom(key) do def remote_domain(Map, :pop!, [_, key], _expected, _meta, _stack, context) when is_atom(key) do
domain = [open_map([{key, {term(), false}}]), term()] domain = [open_map([{key, term()}]), term()]
{{:strong, nil, [{domain, tuple([term(), open_map()])}]}, domain, context} {{:strong, nil, [{domain, tuple([term(), open_map()])}]}, domain, context}
end end
def remote_domain(Map, :update!, [_, key, _], _expected, _meta, _stack, context) def remote_domain(Map, :update!, [_, key, _], _expected, _meta, _stack, context)
when is_atom(key) do when is_atom(key) do
domain = [open_map([{key, {term(), false}}]), term(), fun(1)] domain = [open_map([{key, term()}]), term(), fun(1)]
{{:strong, nil, [{domain, open_map()}]}, domain, context} {{:strong, nil, [{domain, open_map()}]}, domain, context}
end end
@@ -1121,10 +1083,6 @@ defmodule Module.Types.Apply do
end end
end end
defp remote_apply(:erlang, :element, info, [_index, tuple] = args_types, stack) do
remote_apply_tuple_element(info, args_types, tuple, stack)
end
defp remote_apply(:erlang, :map_get, _info, [key, map] = args_types, stack) do defp remote_apply(:erlang, :map_get, _info, [key, map] = args_types, stack) do
case map_get(map, key) do case map_get(map, key) do
{:ok, value} -> {:ok, return(value, args_types, stack)} {:ok, value} -> {:ok, return(value, args_types, stack)}
@@ -1167,38 +1125,26 @@ defmodule Module.Types.Apply do
none() none()
end end
{:ok, return(opt_union(left_result, right_result), [left, right], stack)} {:ok, return(union(left_result, right_result), [left, right], stack)}
:badproperlist -> :badproperlist ->
{:error, badremote(:erlang, :++, [left, right])} {:error, badremote(:erlang, :++, [left, right])}
end end
end end
defp remote_apply(Kernel, :elem, info, [tuple, _index] = args_types, stack) do
remote_apply_tuple_element(info, args_types, tuple, stack)
end
@struct_key atom([:__struct__]) @struct_key atom([:__struct__])
@nil_atom atom([nil]) @nil_atom atom([nil])
defp remote_apply(Map, :from_struct, info, [map] = args_types, stack) do defp remote_apply(Map, :from_struct, _info, [map] = args_types, stack) do
case remote_apply(info, args_types, stack) do case map_update(map, @struct_key, not_set(), false, true) do
{:ok, _type} -> {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
case map_update(map, @struct_key, none(), true, false, true) do :badmap -> {:error, badremote(Map, :from_struct, args_types)}
{_value, descr, _errors} -> {:error, _errors} -> {:ok, map}
{:ok, return(descr, args_types, stack)}
_ ->
{:error, badremote(Map, :from_struct, args_types)}
end
other ->
other
end end
end end
defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do
case map_update(map, key, none(), true, false, true) do case map_update(map, key, not_set(), false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :delete, args_types)} :badmap -> {:error, badremote(Map, :delete, args_types)}
{:error, _errors} -> {:ok, map} {:error, _errors} -> {:ok, map}
@@ -1208,7 +1154,7 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do
case map_get(map, key) do case map_get(map, key) do
{_, value} -> {_, value} ->
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error])) result = tuple([atom([:ok]), value]) |> union(atom([:error]))
{:ok, return(result, args_types, stack)} {:ok, return(result, args_types, stack)}
:badmap -> :badmap ->
@@ -1229,7 +1175,7 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
case map_get(map, key) do case map_get(map, key) do
{:ok, value} -> {:ok, return(opt_union(value, @nil_atom), args_types, stack)} {:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
:badmap -> {:error, badremote(Map, :get, args_types)} :badmap -> {:error, badremote(Map, :get, args_types)}
:error -> {:error, {:badkeydomain, map, key, @nil_atom}} :error -> {:error, {:badkeydomain, map, key, @nil_atom}}
end end
@@ -1237,7 +1183,7 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do
case map_get(map, key) do case map_get(map, key) do
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)} {:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
:badmap -> {:error, badremote(Map, :get, args_types)} :badmap -> {:error, badremote(Map, :get, args_types)}
:error -> {:error, {:badkeydomain, map, key, default}} :error -> {:error, {:badkeydomain, map, key, default}}
end end
@@ -1247,7 +1193,7 @@ defmodule Module.Types.Apply do
case fun_apply(fun, []) do case fun_apply(fun, []) do
{:ok, default} -> {:ok, default} ->
case map_get(map, key) do case map_get(map, key) do
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)} {:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
:badmap -> {:error, badremote(Map, :get_lazy, args_types)} :badmap -> {:error, badremote(Map, :get_lazy, args_types)}
:error -> {:error, {:badkeydomain, map, key, default}} :error -> {:error, {:badkeydomain, map, key, default}}
end end
@@ -1275,9 +1221,9 @@ defmodule Module.Types.Apply do
_ -> args_types _ -> args_types
end end
case map_update(map, key, none(), true, true, false) do case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} -> {value, descr, _errors} ->
value = opt_union(value, default) value = union(value, default)
{:ok, return(tuple([value, descr]), args_types, stack)} {:ok, return(tuple([value, descr]), args_types, stack)}
:badmap -> :badmap ->
@@ -1291,9 +1237,9 @@ defmodule Module.Types.Apply do
defp remote_apply(Map, :pop_lazy, _info, [map, key, fun] = args_types, stack) do defp remote_apply(Map, :pop_lazy, _info, [map, key, fun] = args_types, stack) do
case fun_apply(fun, []) do case fun_apply(fun, []) do
{:ok, default} -> {:ok, default} ->
case map_update(map, key, none(), true, true, false) do case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} -> {value, descr, _errors} ->
value = opt_union(value, default) value = union(value, default)
{:ok, return(tuple([value, descr]), args_types, stack)} {:ok, return(tuple([value, descr]), args_types, stack)}
:badmap -> :badmap ->
@@ -1309,7 +1255,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do
case map_update(map, key, value, false, false, true) do case map_update(map, key, value, false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :put, args_types)} :badmap -> {:error, badremote(Map, :put, args_types)}
{:error, _errors} -> {:ok, map} {:error, _errors} -> {:ok, map}
@@ -1317,7 +1263,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do
case map_update(map, key, none(), true, true, false) do case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)} {value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)}
:badmap -> {:error, badremote(Map, :pop!, args_types)} :badmap -> {:error, badremote(Map, :pop!, args_types)}
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}} {:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
@@ -1325,7 +1271,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(Map, :replace, _info, [map, key, value] = args_types, stack) do defp remote_apply(Map, :replace, _info, [map, key, value] = args_types, stack) do
fun = fn _value, optional? -> {value, optional?} end fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
case map_update_fun(map, key, fun, false, false) do case map_update_fun(map, key, fun, false, false) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
@@ -1335,7 +1281,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(Map, :replace!, _info, [map, key, value] = args_types, stack) do defp remote_apply(Map, :replace!, _info, [map, key, value] = args_types, stack) do
fun = fn _value, optional? -> {value, optional?} end fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
case map_update_fun(map, key, fun, false, false) do case map_update_fun(map, key, fun, false, false) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
@@ -1362,12 +1308,12 @@ defmodule Module.Types.Apply do
_ -> map _ -> map
end end
fun_apply = fn arg_type, optional? -> fun_apply = fn optional?, arg_type ->
if empty?(arg_type) do if empty?(arg_type) do
{default, false} default
else else
case fun_apply(fun, [arg_type]) do case fun_apply(fun, [arg_type]) do
{:ok, res} -> {if(optional?, do: opt_union(res, default), else: res), false} {:ok, res} -> if optional?, do: union(res, default), else: res
reason -> throw({:badapply, reason, [arg_type]}) reason -> throw({:badapply, reason, [arg_type]})
end end
end end
@@ -1391,7 +1337,7 @@ defmodule Module.Types.Apply do
defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do
case map_get(map, key) do case map_get(map, key) do
{_, value} -> {_, value} ->
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error])) result = tuple([atom([:ok]), value]) |> union(atom([:error]))
{:ok, return(result, args_types, stack)} {:ok, return(result, args_types, stack)}
:badmap -> :badmap ->
@@ -1411,6 +1357,7 @@ defmodule Module.Types.Apply do
if key_type == dynamic() or key_type == term() do if key_type == dynamic() or key_type == term() do
{:ok, return(open_map(), args_types, stack)} {:ok, return(open_map(), args_types, stack)}
else else
value_type = if_set(value_type)
domain_keys = to_domain_keys(key_type) domain_keys = to_domain_keys(key_type)
keys = keys =
@@ -1419,18 +1366,12 @@ defmodule Module.Types.Apply do
_ -> [domain_keys] _ -> [domain_keys]
end end
map = map = closed_map(Enum.map(keys, &{&1, value_type}))
closed_map(
Enum.map(keys, fn
key when is_atom(key) -> {key, {value_type, true}}
key -> {key, value_type}
end)
)
map_and_maybe_empty_map = map_and_maybe_empty_map =
case empty_list? do case empty_list? do
true -> map true -> map
false -> opt_difference(map, empty_map()) false -> difference(map, empty_map())
end end
{:ok, return(map_and_maybe_empty_map, args_types, stack)} {:ok, return(map_and_maybe_empty_map, args_types, stack)}
@@ -1457,7 +1398,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(:maps, :put, _info, [key, value, map] = args_types, stack) do defp remote_apply(:maps, :put, _info, [key, value, map] = args_types, stack) do
case map_update(map, key, value, false, false, true) do case map_update(map, key, value, false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(:maps, :put, args_types)} :badmap -> {:error, badremote(:maps, :put, args_types)}
{:error, _errors} -> {:ok, map} {:error, _errors} -> {:ok, map}
@@ -1465,7 +1406,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
case map_update(map, key, none(), true, false, true) do case map_update(map, key, not_set(), false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(:maps, :remove, args_types)} :badmap -> {:error, badremote(:maps, :remove, args_types)}
{:error, _errors} -> {:ok, map} {:error, _errors} -> {:ok, map}
@@ -1473,9 +1414,9 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
case map_update(map, key, none(), true, true, false) do case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} -> {value, descr, _errors} ->
result = opt_union(tuple([value, descr]), atom([:error])) result = union(tuple([value, descr]), atom([:error]))
{:ok, return(result, args_types, stack)} {:ok, return(result, args_types, stack)}
:badmap -> :badmap ->
@@ -1494,7 +1435,7 @@ defmodule Module.Types.Apply do
end end
defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do
fun = fn _value, optional? -> {value, optional?} end fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
case map_update_fun(map, key, fun, false, false) do case map_update_fun(map, key, fun, false, false) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
@@ -1510,49 +1451,10 @@ defmodule Module.Types.Apply do
end end
end end
defp remote_apply(mod, :to_existing_atom, info, [_string, list] = args_types, stack)
when mod in [String, List] do
# TODO: remove once we add parametric types, this will just be:
# binary(), non_empty_list(a) -> a when a: atom()
case remote_apply(info, args_types, stack) do
{:ok, _} ->
# If list is a dynamic type, remote_apply only guarantees compatibility,
# and therefore we may still have improper lists or lists of dynamic().
# Note we cannot have static empty lists, as those would fail on remote_apply,
# but we can have dynamic empty lists (which we should ignore)
case list_of(list) do
{_empty_list, refined_atom} when is_descr(refined_atom) ->
{:ok, return(opt_intersection(refined_atom, atom()), args_types, stack)}
:badproperlist ->
{:error, badremote(mod, :to_existing_atom, args_types)}
end
other ->
other
end
end
defp remote_apply(_mod, _fun, info, args_types, stack) do defp remote_apply(_mod, _fun, info, args_types, stack) do
remote_apply(info, args_types, stack) remote_apply(info, args_types, stack)
end end
defp remote_apply_tuple_element(info, args_types, tuple, stack) do
# TODO: Convert this into a regular type signature {...a} and not {}, integer -> a
with {:ok, fallback} <- remote_apply(info, args_types, stack) do
case tuple_values(tuple) do
:badtuple ->
# This is not virtually possible, since remote apply validates the arguments.
# So returning the fallback is acceptable.
{:ok, fallback}
values ->
{:ok, return(values, args_types, stack)}
end
end
end
defp remote_apply(:none, _args_types, _stack) do defp remote_apply(:none, _args_types, _stack) do
{:ok, dynamic()} {:ok, dynamic()}
end end
@@ -1588,10 +1490,10 @@ defmodule Module.Types.Apply do
Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} -> Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} ->
case signature(module, fun, arity, meta, stack, context) do case signature(module, fun, arity, meta, stack, context) do
{{:strong, _, clauses}, context} -> {{:strong, _, clauses}, context} ->
{opt_union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context} {union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses -> {{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
{opt_union(type, fun_from_inferred_clauses(clauses)), fallback?, context} {union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
{_, context} -> {_, context} ->
{type, true, context} {type, true, context}
@@ -1880,8 +1782,8 @@ defmodule Module.Types.Apply do
defp map_put_new(map, key, value, name, args_types, stack) do defp map_put_new(map, key, value, name, args_types, stack) do
fun = fn fun = fn
type, true -> {opt_union(type, value), false} true, type -> union(type, value)
type, false -> {if(empty?(type), do: value, else: type), false} false, type -> if empty?(type), do: value, else: type
end end
case map_update_fun(map, key, fun, false, true) do case map_update_fun(map, key, fun, false, true) do
@@ -1899,9 +1801,9 @@ defmodule Module.Types.Apply do
_ -> map _ -> map
end end
fun_apply = fn arg_type, optional? -> fun_apply = fn optional?, arg_type ->
case fun_apply(fun, [arg_type]) do case fun_apply(fun, [arg_type]) do
{:ok, res} -> {res, optional?} {:ok, res} -> if optional?, do: if_set(res), else: res
reason -> throw({:badapply, reason, [arg_type]}) reason -> throw({:badapply, reason, [arg_type]})
end end
end end
@@ -1936,7 +1838,7 @@ defmodule Module.Types.Apply do
case filter_domain(clauses, expected, [], true) do case filter_domain(clauses, expected, [], true) do
:none -> domain(domain, clauses) :none -> domain(domain, clauses)
:all -> domain(domain, clauses) :all -> domain(domain, clauses)
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &opt_union/2) end) args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end)
end end
end end
@@ -1960,7 +1862,7 @@ defmodule Module.Types.Apply do
{used, dynamic()} {used, dynamic()}
{_count, used, returns} -> {_count, used, returns} ->
{used, returns |> Enum.reduce(&opt_union/2) |> dynamic()} {used, returns |> Enum.reduce(&union/2) |> dynamic()}
end end
end end
@@ -1977,7 +1879,7 @@ defmodule Module.Types.Apply do
# as a non disjoint check. So we skip checking compatibility twice. # as a non disjoint check. So we skip checking compatibility twice.
with true <- zip_compatible_or_only_gradual?(args_types, domain), with true <- zip_compatible_or_only_gradual?(args_types, domain),
{count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do {count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do
{used, returns |> Enum.reduce(&opt_union/2) |> return(args_types, stack)} {used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)}
else else
_ -> :error _ -> :error
end end
@@ -2322,7 +2224,7 @@ defmodule Module.Types.Apply do
clauses = clauses =
case mod.__protocol__(:impls) do case mod.__protocol__(:impls) do
{:consolidated, mods} -> {:consolidated, mods} ->
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&opt_union/2) domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
[{[domain], dynamic()}] [{[domain], dynamic()}]
_ -> _ ->
@@ -2532,10 +2434,10 @@ defmodule Module.Types.Apply do
term_type?(actual) do term_type?(actual) do
actual |> to_quoted() |> Code.Formatter.to_algebra() actual |> to_quoted() |> Code.Formatter.to_algebra()
else else
common = opt_intersection(actual, expected) common = intersection(actual, expected)
uncommon_doc = uncommon_doc =
opt_difference(actual, expected) difference(actual, expected)
|> to_quoted() |> to_quoted()
|> Code.Formatter.to_algebra() |> Code.Formatter.to_algebra()
|> ansi_red() |> ansi_red()
@@ -2553,17 +2455,17 @@ defmodule Module.Types.Apply do
end end
@composite_types non_empty_list(term(), term()) @composite_types non_empty_list(term(), term())
|> opt_union(tuple()) |> union(tuple())
|> opt_union(open_map()) |> union(open_map())
|> opt_union(fun()) |> union(fun())
# If actual/expected have a composite type, computing the # If actual/expected have a composite type, computing the
# `opt_intersection(actual, expected) or opt_difference(actual, expected)` # `intersection(actual, expected) or difference(actual, expected)`
# can lead to an explosion of terms that actually make debugging # can lead to an explosion of terms that actually make debugging
# harder. So we check that at least one of the two operations # harder. So we check that at least one of the two operations
# return none() (i.e. actual is a subtype or they are disjoint). # return none() (i.e. actual is a subtype or they are disjoint).
defp has_simple_difference?(actual, expected) do defp has_simple_difference?(actual, expected) do
composite_types = opt_intersection(actual, @composite_types) composite_types = intersection(actual, @composite_types)
subtype?(composite_types, expected) or disjoint?(composite_types, expected) subtype?(composite_types, expected) or disjoint?(composite_types, expected)
end end
File diff suppressed because it is too large Load Diff
+129 -128
View File
@@ -11,55 +11,51 @@ defmodule Module.Types.Expr do
14 = length(Macro.Env.__info__(:struct)) 14 = length(Macro.Env.__info__(:struct))
aliases = list(tuple([atom(), atom()])) aliases = list(tuple([atom(), atom()]))
macro_aliases = list(tuple([atom(), tuple([term(), atom()])]))
functions_and_macros = list(tuple([atom(), list(tuple([atom(), integer()]))])) functions_and_macros = list(tuple([atom(), list(tuple([atom(), integer()]))]))
list_of_modules = list(atom()) list_of_modules = list(atom())
@try_catch atom([:error, :exit, :throw]) @try_catch atom([:error, :exit, :throw])
@caller closed_map( @caller closed_map(
__struct__: {atom([Macro.Env]), false}, __struct__: atom([Macro.Env]),
aliases: {aliases, false}, aliases: aliases,
context: {atom([:match, :guard, nil]), false}, context: atom([:match, :guard, nil]),
context_modules: {list_of_modules, false}, context_modules: list_of_modules,
file: {binary(), false}, file: binary(),
function: {opt_union(tuple([atom(), integer()]), atom([nil])), false}, function: union(tuple([atom(), integer()]), atom([nil])),
functions: {functions_and_macros, false}, functions: functions_and_macros,
lexical_tracker: {opt_union(pid(), atom([nil])), false}, lexical_tracker: union(pid(), atom([nil])),
line: {integer(), false}, line: integer(),
macro_aliases: {macro_aliases, false}, macro_aliases: aliases,
macros: {functions_and_macros, false}, macros: functions_and_macros,
module: {atom(), false}, module: atom(),
requires: {list_of_modules, false}, requires: list_of_modules,
tracers: {list_of_modules, false}, tracers: list_of_modules,
versioned_vars: {open_map(), false} versioned_vars: open_map()
) )
# We do not make exception dynamic on purpose. If you do a blank rescue, # 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. # then we will assume you need to statically handle all possible exceptions.
@exception open_map(__struct__: {atom(), false}, __exception__: {term(), false}) @exception open_map(__struct__: atom(), __exception__: term())
args_or_arity = opt_union(list(term()), integer()) args_or_arity = union(list(term()), integer())
custom_info_key = opt_difference(atom(), atom([:file, :line, :error_info]))
extra_info = extra_info =
list( list(
tuple([atom([:file]), opt_union(list(integer()), binary())]) tuple([atom([:file]), list(integer())])
|> opt_union(tuple([atom([:line]), integer()])) |> union(tuple([atom([:line]), integer()]))
|> opt_union(tuple([atom([:error_info]), open_map()])) |> union(tuple([atom([:error_info]), open_map()]))
|> opt_union(tuple([custom_info_key, term()]))
) )
@stacktrace list( @stacktrace list(
opt_union( union(
tuple([atom(), atom(), args_or_arity, extra_info]), tuple([atom(), atom(), args_or_arity, extra_info]),
tuple([fun(), args_or_arity, extra_info]) tuple([fun(), args_or_arity, extra_info])
) )
) )
@falsy atom([false, nil]) @falsy atom([false, nil])
@truthy Module.Types.Descr.opt_negation(atom([false, nil])) @truthy negation(atom([false, nil]))
# :atom # :atom
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom), def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
@@ -110,7 +106,7 @@ defmodule Module.Types.Expr do
of_expr(suffix, tl_type, expr, stack, context) of_expr(suffix, tl_type, expr, stack, context)
end end
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context} {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
end end
# {left, right} # {left, right}
@@ -154,13 +150,13 @@ defmodule Module.Types.Expr do
fn pattern_type, context -> fn pattern_type, context ->
# See if we can use the expected type to further refine the pattern type, # See if we can use the expected type to further refine the pattern type,
# if we cannot, use the pattern type as that will fail later on. # if we cannot, use the pattern type as that will fail later on.
type = opt_intersection(pattern_type, expected) type = intersection(pattern_type, expected)
type = if empty?(type), do: pattern_type, else: type type = if empty?(type), do: pattern_type, else: type
{result, context} = of_expr(right_expr, type, expr, stack, context) {result, context} = of_expr(right_expr, type, expr, stack, context)
# The function may still return a too broad type, # The function may still return a too broad type,
# so we refine once again to assign the most appropriate to the pattern. # so we refine once again to assign the most appropriate to the pattern.
{opt_intersection(result, expected), context} {intersection(result, expected), context}
end end
Pattern.of_match(left_expr, type_fun, match, meta, stack, context) Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
@@ -186,12 +182,12 @@ defmodule Module.Types.Expr do
expected_pairs = expected_pairs =
Enum.flat_map(pairs_types, fn {key_type, _value_type} -> Enum.flat_map(pairs_types, fn {key_type, _value_type} ->
case atom_fetch(key_type) do case atom_fetch(key_type) do
{:finite, [key]} -> [{key, {term(), false}}] {:finite, [key]} -> [{key, term()}]
_ -> [] _ -> []
end end
end) end)
expected = opt_intersection(expected, open_map(expected_pairs)) expected = intersection(expected, open_map(expected_pairs))
{map_type, context} = of_expr(map, expected, expr, stack, context) {map_type, context} = of_expr(map, expected, expr, stack, context)
try do try do
@@ -315,7 +311,7 @@ defmodule Module.Types.Expr do
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false) maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
{_, truthy_context} = {_, truthy_context} =
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :except_none}, context) of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
# Keep the context except the warnings, and compute the body # Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context) truthy_context = reset_warnings(truthy_context, context)
@@ -327,14 +323,14 @@ defmodule Module.Types.Expr do
context = Of.reset_vars(pos_body_context, context) context = Of.reset_vars(pos_body_context, context)
{_, falsy_context} = {_, falsy_context} =
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :except_none}, context) of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
falsy_context = reset_warnings(falsy_context, context) falsy_context = reset_warnings(falsy_context, context)
{neg_body_type, neg_body_context} = {neg_body_type, neg_body_context} =
of_expr(neg_body, expected, expr, stack, falsy_context) of_expr(neg_body, expected, expr, stack, falsy_context)
body_type = opt_union(pos_body_type, neg_body_type) body_type = union(pos_body_type, neg_body_type)
context = context =
cond do cond do
@@ -359,7 +355,7 @@ defmodule Module.Types.Expr do
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?) maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
{_, truthy_context} = {_, truthy_context} =
of_expr(head, @truthy, head, %{stack | reverse_arrow: :except_none}, context) of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
# Keep the context except the warnings, and compute the body # Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context) truthy_context = reset_warnings(truthy_context, context)
@@ -373,12 +369,12 @@ defmodule Module.Types.Expr do
context context
else else
{_, falsy_context} = {_, falsy_context} =
of_expr(head, @falsy, head, %{stack | reverse_arrow: :except_none}, context) of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
reset_warnings(falsy_context, context) reset_warnings(falsy_context, context)
end end
{opt_union(body_type, acc), context} {union(body_type, acc), context}
end) end)
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack) dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
@@ -386,7 +382,7 @@ defmodule Module.Types.Expr do
end end
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context) def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
when stack.reverse_arrow in [:except_none, :include_none] do when stack.reverse_arrow == :use do
version = Keyword.fetch!(meta, :version) version = Keyword.fetch!(meta, :version)
clauses = Map.fetch!(context.reverse_arrows, version) clauses = Map.fetch!(context.reverse_arrows, version)
original = context original = context
@@ -406,7 +402,7 @@ defmodule Module.Types.Expr do
context context
# If there is a single clause, we assume it is always evaluated # If there is a single clause, we assume it is always evaluated
# by doing reverse arrows and incorporating all variables into the context. # by doing reverse arrows and incorporting all variables into the context.
# We have to evaluate the head again but it might have been preferred # We have to evaluate the head again but it might have been preferred
# if we could somehow merge a previously computed context. # if we could somehow merge a previously computed context.
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} -> {_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
@@ -423,7 +419,7 @@ defmodule Module.Types.Expr do
reset_warnings(context, original) reset_warnings(context, original)
{_, filtered} -> {_, filtered} ->
case_expected = Enum.reduce(filtered, none(), &opt_union(elem(&1, 0), &2)) case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context) {_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
reset_warnings(context, original) reset_warnings(context, original)
end end
@@ -465,13 +461,7 @@ defmodule Module.Types.Expr do
# Now we refine the case_expr context and use it to compute the body # Now we refine the case_expr context and use it to compute the body
{_, refined_context} = {_, refined_context} =
of_expr( of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
case_expr,
arg_type,
case_expr,
%{stack | reverse_arrow: :except_none},
context
)
{body_type, context} = {body_type, context} =
of_expr(body, expected, body, stack, reset_warnings(refined_context, context)) of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
@@ -484,22 +474,16 @@ defmodule Module.Types.Expr do
else else
[arg_type] = Pattern.of_domain(trees, stack, context) [arg_type] = Pattern.of_domain(trees, stack, context)
clauses_acc = [{arg_type, body_type, clause} | clauses_acc] clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
{{none?, opt_union(body_type, body_acc), clauses_acc}, context} {{none?, union(body_type, body_acc), clauses_acc}, context}
end end
end) end)
context = context =
if none? or stack.mode != :static do if none? or stack.mode != :static do
head_type = Enum.reduce(clauses_acc, none(), &opt_union(elem(&1, 0), &2)) head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
{_, refined_context} = {_, refined_context} =
of_expr( of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
case_expr,
head_type,
case_expr,
%{stack | reverse_arrow: :except_none},
context
)
reset_warnings(refined_context, context) reset_warnings(refined_context, context)
else else
@@ -562,7 +546,7 @@ defmodule Module.Types.Expr do
end end
{type, context} = of_expr(body, expected, body, stack, context) {type, context} = of_expr(body, expected, body, stack, context)
{opt_union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)} {union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
end) end)
{:catch, clauses}, {acc, context} -> {:catch, clauses}, {acc, context} ->
@@ -580,7 +564,7 @@ defmodule Module.Types.Expr do
end) end)
end end
@timeout_type opt_union(integer(), atom([:infinity])) @timeout_type union(integer(), atom([:infinity]))
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
cache_result(meta, stack, original, fn -> cache_result(meta, stack, original, fn ->
@@ -597,11 +581,11 @@ defmodule Module.Types.Expr do
{body_type, context} = of_expr(body, expected, expr, stack, context) {body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do if compatible?(timeout_type, @timeout_type) do
{opt_union(body_type, acc), Of.reset_vars(context, original)} {union(body_type, acc), Of.reset_vars(context, original)}
else else
error = {:badtimeout, timeout_type, timeout, context} error = {:badtimeout, timeout_type, timeout, context}
context = error(__MODULE__, error, meta, stack, context) context = error(__MODULE__, error, meta, stack, context)
{opt_union(body_type, acc), Of.reset_vars(context, original)} {union(body_type, acc), Of.reset_vars(context, original)}
end end
end) end)
|> dynamic_unless_static(stack) |> dynamic_unless_static(stack)
@@ -625,41 +609,34 @@ defmodule Module.Types.Expr do
else else
# TODO: Use the collectable protocol for the output # TODO: Use the collectable protocol for the output
into = Keyword.get(opts, :into, []) into = Keyword.get(opts, :into, [])
{into_type, into_kind, context} = for_into(into, meta, stack, context)
{into_kinds, into_type, expected, context} = case into_kind do
for_into(into, meta, expected, stack, context) :bitstring ->
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
intersection = intersection(block_type, bitstring())
{block_type, context} = of_expr(block, expected, block, stack, context)
result_type =
if :non_empty_list in into_kinds do
opt_union(into_type, non_empty_list(block_type))
else
into_type
end
{result_type, context} =
if :bitstring in into_kinds do
intersection = opt_intersection(block_type, bitstring())
# TODO: currently if :into is a binary, then we expect the body
# to return a binary, even if the binary is a gradual type. This
# is ok for now because we only check for bitstring if the type
# is a subset of empty_list() or bitstring(), but we may want to
# relax in the future.
if empty?(intersection) do if empty?(intersection) do
error = {:badbitbody, block_type, block, context} error = {:badbitbody, block_type, block, context}
{error_type(), error(__MODULE__, error, meta, stack, context)} {error_type(), error(__MODULE__, error, meta, stack, context)}
else else
# If into_type is a binary but the block is a bitstring_no_binary(), {union(into_type, intersection), context}
# then the result may be a bitstring_no_binary().
{opt_union(intersection, result_type), context}
end end
else
{result_type, context}
end
{if(gradual?(into_type), do: dynamic(result_type), else: result_type), context} :non_empty_list ->
expected =
case list_hd(expected) do
{:ok, head} -> head
_ -> term()
end
{block_type, context} = of_expr(block, expected, block, stack, context)
{union(into_type, non_empty_list(block_type)), context}
:none ->
{_, context} = of_expr(block, term(), block, stack, context)
{into_type, context}
end
|> dynamic_unless_static(stack) |> dynamic_unless_static(stack)
end end
end) end)
@@ -685,7 +662,7 @@ defmodule Module.Types.Expr do
{else_types, context} {else_types, context}
end end
dynamic_unless_static({opt_union(do_result, else_result), context}, stack) dynamic_unless_static({union(do_result, else_result), context}, stack)
end) end)
end end
@@ -702,9 +679,7 @@ defmodule Module.Types.Expr do
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context) def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
when not is_atom(callee) and is_atom(key_or_fun) do when not is_atom(callee) and is_atom(key_or_fun) do
if Keyword.get(meta, :no_parens, false) do if Keyword.get(meta, :no_parens, false) do
{type, context} = {type, context} = of_expr(callee, open_map([{key_or_fun, expected}]), expr, stack, context)
of_expr(callee, open_map([{key_or_fun, {expected, false}}]), expr, stack, context)
Of.map_fetch(call, type, key_or_fun, stack, context) Of.map_fetch(call, type, key_or_fun, stack, context)
else else
{type, context} = of_expr(callee, atom(), call, stack, context) {type, context} = of_expr(callee, atom(), call, stack, context)
@@ -713,6 +688,38 @@ defmodule Module.Types.Expr do
end end
end end
def of_expr(
{{:., _, [:erlang, fun]}, _meta, [left, right]} = call,
expected,
_expr,
stack,
context
)
when fun in [:andalso, :orelse] do
polarity = fun == :andalso
polarity_type = atom([polarity])
left_expected =
case booleaness(expected) do
{^polarity, _} -> polarity_type
_ -> boolean()
end
{left_type, context} = of_expr(left, left_expected, call, stack, context)
{right_type, right_context} = of_expr(right, expected, call, stack, context)
if subtype?(left_type, polarity_type) do
{right_type, right_context}
else
context =
right_context
|> Of.reset_vars(context)
|> reset_warnings(context)
{union(atom([not polarity]), right_type), context}
end
end
def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do
{remote_type, context} = of_expr(remote, atom(), call, stack, context) {remote_type, context} = of_expr(remote, atom(), call, stack, context)
{mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context) {mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context)
@@ -765,7 +772,7 @@ defmodule Module.Types.Expr do
defp of_tuple([elem | elems], index, acc, expected, expr, stack, context) do defp of_tuple([elem | elems], index, acc, expected, expr, stack, context) do
expr_expected = expr_expected =
case tuple_fetch(expected, index) do case tuple_fetch(expected, index) do
{_optional?, type} -> type {_, type} -> type
_ -> term() _ -> term()
end end
@@ -780,7 +787,7 @@ defmodule Module.Types.Expr do
## Try ## Try
defp of_rescue(var, exceptions, expr, info, meta, stack, context) do defp of_rescue(var, exceptions, expr, info, meta, stack, context) do
args = [__exception__: {term(), false}] args = [__exception__: term()]
{structs, context} = {structs, context} =
Enum.map_reduce(exceptions, context, fn exception, context -> Enum.map_reduce(exceptions, context, fn exception, context ->
@@ -800,7 +807,7 @@ defmodule Module.Types.Expr do
context context
_ -> _ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &opt_union/2) expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
expr = {:__block__, [type_check: info], [expr]} expr = {:__block__, [type_check: info], [expr]}
context = Of.declare_var(var, context) context = Of.declare_var(var, context)
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context) {_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
@@ -844,22 +851,15 @@ defmodule Module.Types.Expr do
context context
end end
@into_compile opt_union(bitstring(), empty_list()) @into_compile union(bitstring(), empty_list())
defp maybe_list_hd_or_term(expected) do defp for_into([], _meta, _stack, context),
case list_hd(expected) do do: {empty_list(), :non_empty_list, context}
{:ok, head} -> head
_ -> term()
end
end
defp for_into([], _meta, expected, _stack, context), defp for_into(binary, _meta, _stack, context) when is_binary(binary),
do: {[:non_empty_list], empty_list(), maybe_list_hd_or_term(expected), context} do: {binary(), :bitstring, context}
defp for_into(binary, _meta, _expected, _stack, context) when is_binary(binary), defp for_into(into, meta, stack, context) do
do: {[:bitstring], binary(), bitstring(), context}
defp for_into(into, meta, expected, stack, context) do
meta = meta =
case into do case into do
{_, meta, _} -> meta {_, meta, _} -> meta
@@ -876,26 +876,27 @@ defmodule Module.Types.Expr do
# We use subtype? instead of compatible because we want to handle # We use subtype? instead of compatible because we want to handle
# only bitstring/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 if subtype?(type, @into_compile) do
cond do case {bitstring_type?(type), empty_list_type?(type)} do
bitstring_type?(type) -> # If they can be both be true, then we don't know
kinds = if empty_list_type?(type), do: [:bitstring, :non_empty_list], else: [:bitstring] # what the contents of the block are for
# A comprehension may concatenate the block an arbitrary number of times. {true, true} ->
# Even if both the initial value and each block are unaligned bitstrings, type = union(bitstring(), list(term()))
# repeated concatenation may eventually produce an aligned binary. {if(gradual?(type), do: dynamic(type), else: type), :none, context}
{kinds, opt_union(binary(), type), bitstring(), context}
empty_list_type?(type) -> {false, true} ->
{[:non_empty_list], type, maybe_list_hd_or_term(expected), context} {type, :non_empty_list, context}
# The type is empty... {true, false} ->
true -> {type, :bitstring, context}
{[], type, term(), context}
{false, false} ->
{type, :none, context}
end end
else else
{_type, context} = {_type, context} =
Apply.remote_apply(info, Collectable, :into, [type], expr, stack, context) Apply.remote_apply(info, Collectable, :into, [type], expr, stack, context)
{[], dynamic(), term(), context} {dynamic(), :none, context}
end end
end end
@@ -913,10 +914,10 @@ defmodule Module.Types.Expr do
[pattern_type] = Pattern.of_domain(trees, stack, context) [pattern_type] = Pattern.of_domain(trees, stack, context)
{_, refined_context} = {_, refined_context} =
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :except_none}, context) of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
else_type = if precise?, do: opt_difference(type, pattern_type), else: type else_type = if precise?, do: difference(type, pattern_type), else: type
{opt_union(else_type, else_types), reset_warnings(refined_context, context)} {union(else_type, else_types), reset_warnings(refined_context, context)}
end end
defp with_clause(expr, stack, {else_types, context}) do defp with_clause(expr, stack, {else_types, context}) do
@@ -940,7 +941,7 @@ defmodule Module.Types.Expr do
Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, 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} 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) {type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
{opt_union(acc, type), context} {union(acc, type), context}
end) end)
end end
@@ -983,7 +984,7 @@ defmodule Module.Types.Expr do
context = put_in(context.reverse_arrows[version], result) context = put_in(context.reverse_arrows[version], result)
{result, context} {result, context}
_ -> :use ->
version = Keyword.fetch!(meta, :version) version = Keyword.fetch!(meta, :version)
{Map.fetch!(context.reverse_arrows, version), context} {Map.fetch!(context.reverse_arrows, version), context}
end end
@@ -1004,7 +1005,7 @@ defmodule Module.Types.Expr do
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc -> of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
{body_type, context} = of_expr(body, expected, body, stack, context) {body_type, context} = of_expr(body, expected, body, stack, context)
{opt_union(acc, body_type), context} {union(acc, body_type), context}
end end
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc) of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
@@ -1059,7 +1060,7 @@ defmodule Module.Types.Expr do
do: {left_expr, right_expr} do: {left_expr, right_expr}
defp add_inferred([{args, existing_return} | tail], args, return), defp add_inferred([{args, existing_return} | tail], args, return),
do: [{args, opt_union(existing_return, return)} | tail] do: [{args, union(existing_return, return)} | tail]
defp add_inferred([head | tail], args, return), defp add_inferred([head | tail], args, return),
do: [head | add_inferred(tail, args, return)] do: [head | add_inferred(tail, args, return)]
@@ -1068,7 +1069,7 @@ defmodule Module.Types.Expr do
do: [{args, return}] do: [{args, return}]
defp literal_map_update(descr, key_descr, value_descr) do defp literal_map_update(descr, key_descr, value_descr) do
case map_update(descr, key_descr, value_descr, false, false, false) do case map_update(descr, key_descr, value_descr, false, false) do
{_type, descr, []} -> {:ok, descr} {_type, descr, []} -> {:ok, descr}
{_, _, [error | _]} -> error {_, _, [error | _]} -> error
:badmap -> :badmap :badmap -> :badmap
+1 -1
View File
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
## Warnings ## Warnings
@doc """ @doc """
Converts an integer into ordinal. Converts an itneger into ordinal.
""" """
def integer_to_ordinal(i) do def integer_to_ordinal(i) do
case rem(i, 10) do case rem(i, 10) do
+33 -51
View File
@@ -11,7 +11,7 @@ defmodule Module.Types.Of do
@prefix quote(do: ...) @prefix quote(do: ...)
@suffix quote(do: ...) @suffix quote(do: ...)
@integer_or_float opt_union(integer(), float()) @integer_or_float union(integer(), float())
@integer integer() @integer integer()
@float float() @float float()
@binary binary() @binary binary()
@@ -31,7 +31,7 @@ defmodule Module.Types.Of do
@doc """ @doc """
Marks a variable with error. Marks a variable with error.
This purposely deletes all traces of the variable, This purposedly deletes all traces of the variable,
as it is often invoked when the cause for error is elsewhere. as it is often invoked when the cause for error is elsewhere.
""" """
def error_var({_, meta, _}, context) do def error_var({_, meta, _}, context) do
@@ -106,15 +106,15 @@ defmodule Module.Types.Of do
context context
end end
case stack do case context do
_ when type in @skip_refinement_for or is_map_key(data, :errored) -> _ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context} {old_type, context}
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] -> %{pattern_info: %{guard_context: guard_context}} ->
new_type = opt_intersection(old_type, type) new_type = intersection(old_type, type)
case empty?(new_type) do case empty?(new_type) do
true when reverse_arrow == :include_none -> true when guard_context == :orelse ->
data = %{ data = %{
data data
| type: none(), | type: none(),
@@ -171,7 +171,7 @@ defmodule Module.Types.Of do
{:ok, error_type(), context} {:ok, error_type(), context}
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars -> %{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
new_type = opt_intersection(type, old_type) new_type = intersection(type, old_type)
data = %{ data = %{
data data
@@ -245,7 +245,7 @@ defmodule Module.Types.Of do
type = type =
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc -> Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
%{^version => %{type: type}} = vars %{^version => %{type: type}} = vars
opt_union(acc, type) union(acc, type)
end) end)
{_, context} = refine_body_var(version, type, expr, stack, context) {_, context} = refine_body_var(version, type, expr, stack, context)
@@ -264,8 +264,8 @@ defmodule Module.Types.Of do
{Float, float()}, {Float, float()},
{Function, fun()}, {Function, fun()},
{Integer, integer()}, {Integer, integer()},
{List, opt_union(empty_list(), non_empty_list(term(), term()))}, {List, union(empty_list(), non_empty_list(term(), term()))},
{Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})}, {Map, open_map(__struct__: if_set(negation(atom())))},
{Port, port()}, {Port, port()},
{PID, pid()}, {PID, pid()},
{Reference, reference()}, {Reference, reference()},
@@ -294,7 +294,7 @@ defmodule Module.Types.Of do
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info) struct_type(struct, info)
else else
open_map(__struct__: {atom([struct]), false}) open_map(__struct__: atom([struct]))
end end
end end
@@ -365,7 +365,7 @@ defmodule Module.Types.Of do
# Because a multiple key may override single keys, we can only # Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples. # collect single keys while there are no multiples.
[key] when multiple == [] -> [key] when multiple == [] ->
{dynamic?, domain, [{key, {value_type, false}} | single], multiple} {dynamic?, domain, [{key, value_type} | single], multiple}
_ -> _ ->
{dynamic?, domain, single, [{pos, value_type} | multiple]} {dynamic?, domain, single, [{pos, value_type} | multiple]}
@@ -381,47 +381,37 @@ defmodule Module.Types.Of do
closed_map(non_multiple) closed_map(non_multiple)
[{keys, type} | tail] -> [{keys, type} | tail] ->
products = cartesian_map(tail) for key <- keys, t <- cartesian_map(tail) do
closed_map(non_multiple ++ [{key, type} | t])
for key <- keys, product <- products do
closed_map(non_multiple ++ [{key, {type, false}} | product])
end end
|> Enum.reduce(&opt_union/2) |> Enum.reduce(&union/2)
end end
{if(dynamic?, do: dynamic(map), else: map), context} {if(dynamic?, do: dynamic(map), else: map), context}
end end
defp union_negated([], new_type, single, multiple) do defp union_negated([], new_type, single, multiple) do
single = single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
Enum.map(single, fn multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
{key, {old_type, optional?}} ->
{key, {opt_union(old_type, new_type), optional?}}
end)
multiple =
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end)
{single, multiple} {single, multiple}
end end
defp union_negated(negated, new_type, single, multiple) do defp union_negated(negated, new_type, single, multiple) do
{single, matched} = {single, matched} =
Enum.map_reduce(single, [], fn Enum.map_reduce(single, [], fn {key, old_type}, matched ->
{key, {old_type, optional?}}, matched -> if key in negated do
if key in negated do {{key, old_type}, [key | matched]}
{{key, {old_type, optional?}}, [key | matched]} else
else {{key, union(old_type, new_type)}, matched}
{{key, {opt_union(old_type, new_type), optional?}}, matched} end
end
end) end)
multiple = multiple =
Enum.map(multiple, fn {keys, old_type} -> Enum.map(multiple, fn {keys, old_type} ->
{keys, opt_union(old_type, new_type)} {keys, union(old_type, new_type)}
end) end)
{Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple} {Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
end end
defp pairs(pairs, expected, stack, context, of_fun) do defp pairs(pairs, expected, stack, context, of_fun) do
@@ -430,7 +420,7 @@ defmodule Module.Types.Of do
expected_value_type = expected_value_type =
with {[key], [], []} <- pos_neg_domain, with {[key], [], []} <- pos_neg_domain,
{_optional?, expected_value_type} <- map_fetch_key(expected, key) do {_, expected_value_type} <- map_fetch_key(expected, key) do
expected_value_type expected_value_type
else else
_ -> term() _ -> term()
@@ -465,8 +455,7 @@ defmodule Module.Types.Of do
[[]] [[]]
[{keys, type} | tail] -> [{keys, type} | tail] ->
products = cartesian_map(tail) for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
for key <- keys, product <- products, do: [{key, {type, false}} | product]
end end
end end
@@ -488,15 +477,15 @@ defmodule Module.Types.Of do
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type = value_type =
case map_fetch_key(expected, key) do case map_fetch_key(expected, key) do
{_optional?, expected_value_type} -> expected_value_type {_, expected_value_type} -> expected_value_type
_ -> term() _ -> term()
end end
{type, context} = of_fun.(value, value_type, stack, context) {type, context} = of_fun.(value, value_type, stack, context)
{{key, {type, false}}, context} {{key, type}, context}
end) end)
{closed_map([__struct__: {atom([struct]), false}] ++ args_types), context} {closed_map([{:__struct__, atom([struct])} | args_types]), context}
end end
end end
@@ -535,16 +524,9 @@ defmodule Module.Types.Of do
# we introduce typed structs. They are only used by exceptions. # we introduce typed structs. They are only used by exceptions.
def struct_type(struct, info, args_types \\ []) do def struct_type(struct, info, args_types \\ []) do
term = dynamic() term = dynamic()
pairs = for %{field: field} <- info, do: {field, {term, false}} pairs = for %{field: field} <- info, do: {field, term}
pairs = [{:__struct__, {atom([struct]), false}} | pairs] pairs = [{:__struct__, atom([struct])} | pairs]
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
pairs =
if args_types == [] do
pairs
else
pairs ++ args_types
end
closed_map(pairs) closed_map(pairs)
end end
+32 -39
View File
@@ -27,7 +27,7 @@ defmodule Module.Types.Pattern do
do: previous do: previous
defp concat_previous(types, {list, descr}), defp concat_previous(types, {list, descr}),
do: {[types | list], opt_union(descr, args_to_previous(types))} do: {[types | list], union(descr, args_to_previous(types))}
defp args_to_previous([type]), do: upper_bound(type) defp args_to_previous([type]), do: upper_bound(type)
defp args_to_previous(types), do: args_to_static_domain(types) defp args_to_previous(types), do: args_to_static_domain(types)
@@ -39,8 +39,8 @@ defmodule Module.Types.Pattern do
defp of_pattern_previous(types, {_, descr}, stack) do defp of_pattern_previous(types, {_, descr}, stack) do
refined_types = refined_types =
case types do case types do
[type] -> [opt_difference(type, descr)] [type] -> [difference(type, descr)]
[_ | _] -> args_to_domain(types) |> opt_difference(descr) |> domain_to_flat_args(types) [_ | _] -> args_to_domain(types) |> difference(descr) |> domain_to_flat_args(types)
end end
# check_previous? is an optimization. If types have not changed, # check_previous? is an optimization. If types have not changed,
@@ -349,7 +349,7 @@ defmodule Module.Types.Pattern do
defp of_pattern_intersect([head | tail], index, acc, pattern_info, tag, stack, context) do defp of_pattern_intersect([head | tail], index, acc, pattern_info, tag, stack, context) do
{tree, expected, pattern} = head {tree, expected, pattern} = head
actual = of_pattern_tree(tree, stack, context) actual = of_pattern_tree(tree, stack, context)
type = opt_intersection(actual, expected) type = intersection(actual, expected)
if empty?(type) do if empty?(type) do
context = badpattern_error(pattern, index, tag, stack, context) context = badpattern_error(pattern, index, tag, stack, context)
@@ -498,7 +498,7 @@ defmodule Module.Types.Pattern do
defp of_pattern_var([{:subpattern, key} | rest], type, context) do defp of_pattern_var([{:subpattern, key} | rest], type, context) do
%{^key => subpattern} = context.subpatterns %{^key => subpattern} = context.subpatterns
of_pattern_var(rest, opt_intersection(type, subpattern), context) of_pattern_var(rest, intersection(type, subpattern), context)
end end
defp of_pattern_var([:tail | rest], type, context) do defp of_pattern_var([:tail | rest], type, context) do
@@ -518,7 +518,7 @@ defmodule Module.Types.Pattern do
# This logic mirrors the code in `Apply.compare` # This logic mirrors the code in `Apply.compare`
cond do cond do
# If it is a singleton, we can always be precise # If it is a singleton, we can always be precise
singleton?(type) -> if polarity, do: type, else: Module.Types.Descr.opt_negation(type) singleton?(type) -> if polarity, do: type, else: negation(type)
# We are checking for `not x == 1` or similar, we can't say anything about x # We are checking for `not x == 1` or similar, we can't say anything about x
polarity == false -> term() polarity == false -> term()
# We are checking for `x == 1`, make sure x is integer or float # We are checking for `x == 1`, make sure x is integer or float
@@ -534,18 +534,14 @@ defmodule Module.Types.Pattern do
defp of_pattern_tree({:open_map, static, dynamic}, stack, context) do defp of_pattern_tree({:open_map, static, dynamic}, stack, context) do
dynamic = dynamic =
Enum.map(dynamic, fn {key, value} -> Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, stack, context)} end)
{key, {of_pattern_tree(value, stack, context), false}}
end)
open_map(static ++ dynamic) open_map(static ++ dynamic)
end end
defp of_pattern_tree({:closed_map, static, dynamic}, stack, context) do defp of_pattern_tree({:closed_map, static, dynamic}, stack, context) do
dynamic = dynamic =
Enum.map(dynamic, fn {key, value} -> Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, stack, context)} end)
{key, {of_pattern_tree(value, stack, context), false}}
end)
closed_map(static ++ dynamic) closed_map(static ++ dynamic)
end end
@@ -554,7 +550,7 @@ defmodule Module.Types.Pattern do
tail tail
|> Enum.reduce( |> Enum.reduce(
of_pattern_tree(head, stack, context), of_pattern_tree(head, stack, context),
&opt_union(of_pattern_tree(&1, stack, context), &2) &union(of_pattern_tree(&1, stack, context), &2)
) )
|> non_empty_list(of_pattern_tree(suffix, stack, context)) |> non_empty_list(of_pattern_tree(suffix, stack, context))
end end
@@ -562,7 +558,7 @@ defmodule Module.Types.Pattern do
defp of_pattern_tree({:intersection, entries}, stack, context) do defp of_pattern_tree({:intersection, entries}, stack, context) do
entries entries
|> Enum.map(&of_pattern_tree(&1, stack, context)) |> Enum.map(&of_pattern_tree(&1, stack, context))
|> Enum.reduce(&opt_intersection/2) |> Enum.reduce(&intersection/2)
end end
defp of_pattern_tree({:var, version}, _stack, context) do defp of_pattern_tree({:var, version}, _stack, context) do
@@ -694,7 +690,7 @@ defmodule Module.Types.Pattern do
if static == [] do if static == [] do
{:intersection, dynamic} {:intersection, dynamic}
else else
{:intersection, [Enum.reduce(static, &opt_intersection/2) | dynamic]} {:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}
end end
context = of_var(var, version, [%{root: return, expr: match}], context) context = of_var(var, version, [%{root: return, expr: match}], context)
@@ -725,20 +721,20 @@ defmodule Module.Types.Pattern do
pairs = Map.new(pairs) pairs = Map.new(pairs)
term = term() term = term()
static = [__struct__: {atom([struct]), false}] static = [__struct__: atom([struct])]
dynamic = [] dynamic = []
{static, dynamic} = {static, dynamic} =
Enum.reduce(info, {static, dynamic}, fn %{field: field}, {static, dynamic} -> Enum.reduce(info, {static, dynamic}, fn %{field: field}, {static, dynamic} ->
case pairs do case pairs do
%{^field => value_type} when is_descr(value_type) -> %{^field => value_type} when is_descr(value_type) ->
{[{field, {value_type, false}} | static], dynamic} {[{field, value_type} | static], dynamic}
%{^field => value_type} -> %{^field => value_type} ->
{static, [{field, value_type} | dynamic]} {static, [{field, value_type} | dynamic]}
_ -> _ ->
{[{field, {term, false}} | static], dynamic} {[{field, term} | static], dynamic}
end end
end) end)
@@ -776,9 +772,8 @@ defmodule Module.Types.Pattern do
{refined, context} = of_match_var(var, atom(), expr, stack, context) {refined, context} = of_match_var(var, atom(), expr, stack, context)
if compatible?(refined, atom()) do if compatible?(refined, atom()) do
fields = [__struct__: {refined, false}]
precise? = match?({:_, _, _}, var) or singleton?(refined) precise? = match?({:_, _, _}, var) or singleton?(refined)
of_open_map(args, precise?, fields, [], path, stack, context) of_open_map(args, precise?, [__struct__: refined], [], path, stack, context)
else else
error = {:badstruct, refined, expr, context} error = {:badstruct, refined, expr, context}
{error_type(), false, error(__MODULE__, error, meta, stack, context)} {error_type(), false, error(__MODULE__, error, meta, stack, context)}
@@ -812,12 +807,8 @@ defmodule Module.Types.Pattern do
version = Keyword.fetch!(meta, :version) version = Keyword.fetch!(meta, :version)
case Enum.reverse(reverse_path) do case Enum.reverse(reverse_path) do
[%{root: :key} | _] -> [%{root: :key} | _] -> {Of.var(var, context), false, context}
{Of.var(var, context), false, context} path -> {{:var, version}, false, of_shared_var(var, version, true, path, context)}
path ->
precise? = singleton?(context.vars[version].type)
{{:var, version}, precise?, of_shared_var(var, version, true, path, context)}
end end
end end
@@ -871,7 +862,7 @@ defmodule Module.Types.Pattern do
precise? = precise? and value_precise? precise? = precise? and value_precise?
if is_descr(value_type) do if is_descr(value_type) do
{precise?, [{key, {value_type, false}} | static], dynamic, context} {precise?, [{key, value_type} | static], dynamic, context}
else else
{precise?, static, [{key, value_type} | dynamic], context} {precise?, static, [{key, value_type} | dynamic], context}
end end
@@ -961,13 +952,13 @@ defmodule Module.Types.Pattern do
type = type =
case {static, dynamic} do case {static, dynamic} do
{static, []} when is_descr(suffix_type) -> {static, []} when is_descr(suffix_type) ->
non_empty_list(Enum.reduce(static, &opt_union/2), suffix_type) non_empty_list(Enum.reduce(static, &union/2), suffix_type)
{[], dynamic} -> {[], dynamic} ->
{:non_empty_list, dynamic, suffix_type} {:non_empty_list, dynamic, suffix_type}
{static, dynamic} -> {static, dynamic} ->
{:non_empty_list, [Enum.reduce(static, &opt_union/2) | dynamic], suffix_type} {:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix_type}
end end
{type, false, context} {type, false, context}
@@ -1036,10 +1027,9 @@ defmodule Module.Types.Pattern do
end end
defp of_guards(guards, vars, stack, context) do defp of_guards(guards, vars, stack, context) do
stack = %{stack | reverse_arrow: :except_none}
context = context =
init_pattern_info(context, %{ init_pattern_info(context, %{
guard_context: :andalso,
parent_version: nil, parent_version: nil,
vars: vars, vars: vars,
subpatterns_vars: %{}, subpatterns_vars: %{},
@@ -1084,6 +1074,10 @@ defmodule Module.Types.Pattern do
%{context | pattern_info: %{pattern_info | parent_version: parent_version}}} %{context | pattern_info: %{pattern_info | parent_version: parent_version}}}
end end
defp enable_conditional_mode(%{pattern_info: pattern_info} = context) do
%{context | pattern_info: %{pattern_info | guard_context: :orelse}, conditional_vars: %{}}
end
defp maybe_badguard(type, guard, stack, context) do defp maybe_badguard(type, guard, stack, context) do
case booleaness(type) do case booleaness(type) do
:maybe_both -> :maybe_both ->
@@ -1135,7 +1129,7 @@ defmodule Module.Types.Pattern do
Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2)) Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2))
{suffix, context} = of_guard(suffix, term(), expr, stack, context) {suffix, context} = of_guard(suffix, term(), expr, stack, context)
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context} {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
end end
# {left, right} # {left, right}
@@ -1175,7 +1169,7 @@ defmodule Module.Types.Pattern do
# var.field # var.field
def of_guard({{:., _, [callee, key]}, _, []} = map_fetch, expected, expr, stack, context) def of_guard({{:., _, [callee, key]}, _, []} = map_fetch, expected, expr, stack, context)
when not is_atom(callee) do when not is_atom(callee) do
{type, context} = of_guard(callee, open_map([{key, {expected, false}}]), expr, stack, context) {type, context} = of_guard(callee, open_map([{key, expected}]), expr, stack, context)
Of.map_fetch(map_fetch, type, key, stack, context) Of.map_fetch(map_fetch, type, key, stack, context)
end end
@@ -1276,7 +1270,7 @@ defmodule Module.Types.Pattern do
# `:fail` as dynamic as its whole purpose is to cause failures. # `:fail` as dynamic as its whole purpose is to cause failures.
defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do
{type, context} = of_guard(left, expected, left, stack, context) {type, context} = of_guard(left, expected, left, stack, context)
{opt_union(type, @dynamic_fail), context} {union(type, @dynamic_fail), context}
end end
defp of_remote(fun, _args, call, expected, stack, context) defp of_remote(fun, _args, call, expected, stack, context)
@@ -1309,8 +1303,7 @@ defmodule Module.Types.Pattern do
of_guard(left, expected, left, stack, context) of_guard(left, expected, left, stack, context)
true -> true ->
cond_context = %{context | conditional_vars: %{}} cond_context = enable_conditional_mode(context)
stack = %{stack | reverse_arrow: :include_none}
# Compute the sure types, which are stored directly in the context # Compute the sure types, which are stored directly in the context
{_type, context} = of_guard(left, boolean(), left, stack, context) {_type, context} = of_guard(left, boolean(), left, stack, context)
@@ -1402,7 +1395,7 @@ defmodule Module.Types.Pattern do
Apply.remote(:erlang, fun, args, expected, call, stack, context, &of_guard/5) Apply.remote(:erlang, fun, args, expected, call, stack, context, &of_guard/5)
end end
defp unpack_op({{:., _, [_mod, fun]}, _, [left, right]}, fun, acc) do defp unpack_op({{:., _, [:erlang, fun]}, _, [left, right]}, fun, acc) do
unpack_op(left, fun, unpack_op(right, fun, acc)) unpack_op(left, fun, unpack_op(right, fun, acc))
end end
@@ -1448,7 +1441,7 @@ defmodule Module.Types.Pattern do
case disjoint? do case disjoint? do
true -> {type, context} true -> {type, context}
false -> {opt_union(to_abort, type), context} false -> {union(to_abort, type), context}
end end
end end
@@ -1463,7 +1456,7 @@ defmodule Module.Types.Pattern do
of_guard(head, expected, head, stack, context) of_guard(head, expected, head, stack, context)
acc_vars = [{vars, cond_vars} | acc_vars] acc_vars = [{vars, cond_vars} | acc_vars]
of_logical_cond(tail, opt_union(acc_type, type), acc_vars, expected, stack, context) of_logical_cond(tail, union(acc_type, type), acc_vars, expected, stack, context)
end end
defp of_logical_cond([], acc_type, acc_vars, _expected, _stack, _context) do defp of_logical_cond([], acc_type, acc_vars, _expected, _stack, _context) do
+4 -2
View File
@@ -39,7 +39,7 @@ defmodule Node do
Currently supported options are: Currently supported options are:
* `:name_domain` - determines the host name part of the node name. If `:longnames`, * `:name_domain` - determines the host name part of the node name. If `:longnames`,
fully qualified domain names will be used, which is also the default. fully qualified domain names will be used which also is the default.
If `:shortnames`, only the short name of the host will be used. If `:shortnames`, only the short name of the host will be used.
* `:net_ticktime` - The tick time to use in seconds. Defaults to the value of the * `:net_ticktime` - The tick time to use in seconds. Defaults to the value of the
@@ -47,7 +47,7 @@ defmodule Node do
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html) See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
for more information. for more information.
* `:net_tickintensity` - The tick intensity to use. Defaults to the value of the * `net_tickintensity` - The tick intensity to use. Defaults to the value of the
`net_tickintensity` configuration under Erlang's `kernel` application. `net_tickintensity` configuration under Erlang's `kernel` application.
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html) See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
for more information. for more information.
@@ -338,6 +338,8 @@ defmodule Node do
The default node is `Node.self/0`, the local node. If `node` is the local node, The default node is `Node.self/0`, the local node. If `node` is the local node,
the function also sets the cookie of all other unknown nodes to `cookie`. the function also sets the cookie of all other unknown nodes to `cookie`.
This function will raise `FunctionClauseError` if the given `node` is not alive.
""" """
@spec set_cookie(t, atom) :: true @spec set_cookie(t, atom) :: true
def set_cookie(node \\ Node.self(), cookie) when is_atom(cookie) do def set_cookie(node \\ Node.self(), cookie) when is_atom(cookie) do
+4 -4
View File
@@ -43,7 +43,7 @@ defmodule OptionParser do
defmodule ParseError do defmodule ParseError do
@moduledoc """ @moduledoc """
An exception raised when parsing an option fails. An exception raised when parsing option fails.
For example, see `OptionParser.parse!/2`. For example, see `OptionParser.parse!/2`.
""" """
@@ -387,7 +387,7 @@ defmodule OptionParser do
do_parse(rest, config, new_opts, args, invalid, all?) do_parse(rest, config, new_opts, args, invalid, all?)
{:invalid, option, value, rest} -> {:invalid, option, value, rest} ->
# the option exists but it has wrong value # the option exist but it has wrong value
do_parse(rest, config, opts, args, [{option, value} | invalid], all?) do_parse(rest, config, opts, args, [{option, value} | invalid], all?)
{:undefined, option, _value, rest} -> {:undefined, option, _value, rest} ->
@@ -463,7 +463,7 @@ defmodule OptionParser do
defp next_with_config(["--" <> option | rest], config) do defp next_with_config(["--" <> option | rest], config) do
{option, value} = split_option(option) {option, value} = split_option(option)
if String.contains?(option, "_") do if String.contains?(option, ["_"]) do
{:undefined, "--" <> option, value, rest} {:undefined, "--" <> option, value, rest}
else else
tagged = tag_option(option, config) tagged = tag_option(option, config)
@@ -852,7 +852,7 @@ defmodule OptionParser do
|> to_existing_key(allow_nonexistent_atoms?) |> to_existing_key(allow_nonexistent_atoms?)
end end
defp to_existing_key(option, true), do: String.to_unsafe_atom(option) defp to_existing_key(option, true), do: String.to_atom(option)
defp to_existing_key(option, false) do defp to_existing_key(option, false) do
try do try do
-26
View File
@@ -699,32 +699,6 @@ defmodule Path do
end end
end end
@doc """
Safely joins two paths.
Returns `{:ok, path}` if `right` is safe to append to `left`, or `:error`
otherwise. See `safe_relative/2` for the exact safety rules applied to `right`.
## Examples
iex> Path.safe_join("foo", "bar")
{:ok, "foo/bar"}
iex> Path.safe_join("foo", "../bar")
:error
iex> Path.safe_join("foo", "/bar")
:error
"""
@doc since: "1.21.0"
@spec safe_join(t, t) :: {:ok, t} | :error
def safe_join(left, right) do
with {:ok, right} <- safe_relative(right, left) do
{:ok, join(left, right)}
end
end
@doc ~S""" @doc ~S"""
Splits the path into a list at the path separator. Splits the path into a list at the path separator.
+6 -6
View File
@@ -61,7 +61,7 @@ defmodule Port do
* `{pid, {:connect, new_pid}}` - sets the `new_pid` as the new owner of * `{pid, {:connect, new_pid}}` - sets the `new_pid` as the new owner of
the port. Once a port is opened, the port is linked and connected to the the port. Once a port is opened, the port is linked and connected to the
caller process and communication to the port only happens through the caller process and communication to the port only happens through the
connected process. This message makes `new_pid` the new connected process. connected process. This message makes `new_pid` the new connected processes.
Unless the port is dead, the port will reply to the old owner with Unless the port is dead, the port will reply to the old owner with
`{port, :connected}`. See `connect/2`. `{port, :connected}`. See `connect/2`.
@@ -88,10 +88,10 @@ defmodule Port do
> On Unix systems, arguments are passed to a new operating system > On Unix systems, arguments are passed to a new operating system
> process as an array of strings but on Windows it is up to the child > process as an array of strings but on Windows it is up to the child
> process to parse them and some Windows programs may apply their own > process to parse them and some Windows programs may apply their own
> rules, which are inconsistent with the standard C runtime `argv` parsing. > rules, which are inconsistent with the standard C runtime `argv` parsing
> >
> This is particularly troublesome when invoking `.bat`, `.cmd`, or `.com` > This is particularly troublesome when invoking `.bat` or `.com` files
> files as these run implicitly through `cmd.exe`, whose argument parsing is > as these run implicitly through `cmd.exe`, whose argument parsing is
> vulnerable to malicious input and can be used to run arbitrary shell > vulnerable to malicious input and can be used to run arbitrary shell
> commands. > commands.
> >
@@ -99,7 +99,7 @@ defmodule Port do
> files or `.com` applications, you must not pass untrusted input as > files or `.com` applications, you must not pass untrusted input as
> arguments to the program. You may avoid accidentally executing them > arguments to the program. You may avoid accidentally executing them
> by explicitly passing the extension of the program you want to run, > by explicitly passing the extension of the program you want to run,
> such as `.exe`, and double-checking the program is indeed not a batch > such as `.exe`, and double check the program is indeed not a batch
> file or `.com` application. > file or `.com` application.
> >
> This affects both `spawn` and `spawn_executable`. > This affects both `spawn` and `spawn_executable`.
@@ -156,7 +156,7 @@ defmodule Port do
We do not always have control over how third-party software terminates. We do not always have control over how third-party software terminates.
If necessary, one workaround is to wrap the child application in a script that If necessary, one workaround is to wrap the child application in a script that
checks whether stdin has been closed. Here is such a script that has been checks whether stdin has been closed. Here is such a script that has been
verified to work on bash shells: verified to work on bash shells:
#!/usr/bin/env bash #!/usr/bin/env bash
+6 -11
View File
@@ -79,11 +79,10 @@ defmodule Process do
@typedoc """ @typedoc """
A process destination. A process destination.
A remote or local PID, a local port, a process alias, a locally registered name, A remote or local PID, a local port, a locally registered name, or a tuple in
or a tuple in the form of `{registered_name, node}` for a registered name at the form of `{registered_name, node}` for a registered name at another node.
another node.
""" """
@type dest :: pid | port | alias | (registered_name :: atom) | {registered_name :: atom, node} @type dest :: pid | port | (registered_name :: atom) | {registered_name :: atom, node}
@doc """ @doc """
Tells whether the given process is alive on the local node. Tells whether the given process is alive on the local node.
@@ -227,7 +226,7 @@ defmodule Process do
> >
> The functions `Kernel.exit/1` and `Process.exit/2` are > The functions `Kernel.exit/1` and `Process.exit/2` are
> named similarly but provide very different functionalities. The > named similarly but provide very different functionalities. The
> `Kernel.exit/1` function should be used when the intent is to stop the current > `Kernel:exit/1` function should be used when the intent is to stop the current
> process while `Process.exit/2` should be used when the intent is to send an > process while `Process.exit/2` should be used when the intent is to send an
> exit signal to another process. Note also that `Kernel.exit/1` can be caught > exit signal to another process. Note also that `Kernel.exit/1` can be caught
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and > with `try/1` while `Process.exit/2` can only be handled by trapping exits and
@@ -340,7 +339,7 @@ defmodule Process do
@doc """ @doc """
Sends a message to the given `dest`. Sends a message to the given `dest`.
`dest` may be a remote or local PID, a local port, a process alias, a locally `dest` may be a remote or local PID, a local port, a locally
registered name, or a tuple in the form of `{registered_name, node}` for a registered name, or a tuple in the form of `{registered_name, node}` for a
registered name at another node. registered name at another node.
@@ -831,16 +830,12 @@ defmodule Process do
Inlined by the compiler. Inlined by the compiler.
""" """
@spec flag(:async_dist, boolean) :: boolean
@spec flag(:error_handler, module) :: module @spec flag(:error_handler, module) :: module
@spec flag(:fullsweep_after, non_neg_integer) :: non_neg_integer
@spec flag(:max_heap_size, heap_size) :: heap_size @spec flag(:max_heap_size, heap_size) :: heap_size
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported # :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap @spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer @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(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:monitor_nodes, term) :: term
@spec flag({:monitor_nodes, term}, term) :: term
@spec flag(:priority, priority_level) :: priority_level @spec flag(:priority, priority_level) :: priority_level
@spec flag(:save_calls, 0..10_000) :: 0..10_000 @spec flag(:save_calls, 0..10_000) :: 0..10_000
@spec flag(:sensitive, boolean) :: boolean @spec flag(:sensitive, boolean) :: boolean
@@ -1007,7 +1002,7 @@ defmodule Process do
end end
@doc """ @doc """
Adds a descriptive term to the current process. Add a descriptive term to the current process.
The term does not need to be unique, and will be shown in Observer and in The term does not need to be unique, and will be shown in Observer and in
crash logs. crash logs.
+6 -8
View File
@@ -215,7 +215,7 @@ defmodule Protocol do
... ...
end end
If you are using `Mix.install/2`, you can do so by passing the `consolidate_protocols` If you are using `Mix.install/2`, you can do by passing the `consolidate_protocols`
option: option:
Mix.install( Mix.install(
@@ -282,9 +282,7 @@ defmodule Protocol do
type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity)) type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
type_args = [quote(do: t) | type_args] type_args = [quote(do: t) | type_args]
to_var = fn pos -> to_var = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end
Macro.var(String.to_unsafe_atom("arg" <> Integer.to_string(pos)), __MODULE__)
end
call_args = :lists.map(to_var, :lists.seq(2, arity)) call_args = :lists.map(to_var, :lists.seq(2, arity))
call_args = [quote(do: term) | call_args] call_args = [quote(do: term) | call_args]
@@ -691,15 +689,15 @@ defmodule Protocol do
structs_domain = structs_domain =
case structs do case structs do
[] -> Descr.none() [] -> Descr.none()
_ -> Descr.open_map(__struct__: {Descr.atom(structs), false}) _ -> Descr.open_map(__struct__: Descr.atom(structs))
end end
domain = domain =
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc -> Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
Descr.opt_union(Module.Types.Of.impl(impl, :open), acc) Descr.union(Module.Types.Of.impl(impl, :open), acc)
end) end)
not_domain = Descr.opt_negation(domain) not_domain = Descr.negation(domain)
if Any in types do if Any in types do
clauses = clauses =
@@ -1230,7 +1228,7 @@ defmodule Protocol do
end end
def __concat__(left, right) when is_binary(right) do def __concat__(left, right) when is_binary(right) do
String.to_unsafe_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right) String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
end end
defp ensure_prefix("Elixir." <> _ = left), do: left defp ensure_prefix("Elixir." <> _ = left), do: left
+9 -20
View File
@@ -258,7 +258,7 @@ defmodule Range do
def size(range) def size(range)
def size(first..last//step) when step > 0 and first > last, do: 0 def size(first..last//step) when step > 0 and first > last, do: 0
def size(first..last//step) when step < 0 and first < last, do: 0 def size(first..last//step) when step < 0 and first < last, do: 0
def size(first..last//step), do: div(last - first, step) + 1 def size(first..last//step), do: abs(div(last - first, step)) + 1
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
def size(%{__struct__: Range, first: first, last: last} = range) do def size(%{__struct__: Range, first: first, last: last} = range) do
@@ -476,8 +476,8 @@ defmodule Range do
""" """
@doc since: "1.8.0" @doc since: "1.8.0"
@spec disjoint?(t, t) :: boolean @spec disjoint?(t, t) :: boolean
def disjoint?(first1..last1//step1, first2..last2//step2) do def disjoint?(first1..last1//step1 = range1, first2..last2//step2 = range2) do
if empty?(first1, last1, step1) or empty?(first2, last2, step2) do if size(range1) == 0 or size(range2) == 0 do
true true
else else
{first1, last1, step1} = normalize(first1, last1, step1) {first1, last1, step1} = normalize(first1, last1, step1)
@@ -487,7 +487,7 @@ defmodule Range do
last2 < first1 or last1 < first2 -> last2 < first1 or last1 < first2 ->
true true
step1 == 1 and step2 == 1 -> abs(step1) == 1 and abs(step2) == 1 ->
false false
true -> true ->
@@ -498,9 +498,9 @@ defmodule Range do
if rem(first2 - first1, gcd) == 0 do if rem(first2 - first1, gcd) == 0 do
c = first1 - first2 + step2 - step1 c = first1 - first2 + step2 - step1
t1 = Integer.floor_div(-c * u, step2) t1 = -c / step2 * u
t2 = Integer.floor_div(-c * v, step1) t2 = -c / step1 * v
t = max(t1 + 1, t2 + 1) t = max(floor(t1) + 1, floor(t2) + 1)
x = div(c * u + t * step2, gcd) - 1 x = div(c * u + t * step2, gcd) - 1
y = div(c * v + t * step1, gcd) - 1 y = div(c * v + t * step1, gcd) - 1
@@ -513,20 +513,9 @@ defmodule Range do
end end
end end
@compile inline: [empty?: 3, normalize: 3] @compile inline: [normalize: 3]
defp empty?(first, last, step) do
case step > 0 do
true -> first > last
false -> first < last
end
end
defp normalize(first, last, step) when first > last, defp normalize(first, last, step) when first > last,
do: {first - div(first - last, step) * step, first, -step} do: {first - abs(div(first - last, step) * step), first, -step}
# A single-element range holds the same element regardless of the step, so
# make the step positive to keep the progression in disjoint?/2 increasing.
defp normalize(first, last, step) when first == last, do: {first, last, abs(step)}
defp normalize(first, last, step), do: {first, last, step} defp normalize(first, last, step), do: {first, last, step}
+1 -1
View File
@@ -340,7 +340,7 @@ defmodule Record do
fields fields
end end
# Normalizes record fields to have default values. # Normalizes of record fields to have default values.
defp fields(kind, fields) do defp fields(kind, fields) do
normalizer_fun = fn normalizer_fun = fn
{key, value} when is_atom(key) -> {key, value} when is_atom(key) ->
+1 -1
View File
@@ -40,7 +40,7 @@ defmodule Record.Extractor do
defp from_lib_file(file) do defp from_lib_file(file) do
[app | path] = :filename.split(String.to_charlist(file)) [app | path] = :filename.split(String.to_charlist(file))
case :code.lib_dir(List.to_unsafe_atom(app)) do case :code.lib_dir(List.to_atom(app)) do
{:error, _} -> {:error, _} ->
raise ArgumentError, "lib file #{file} could not be found" raise ArgumentError, "lib file #{file} could not be found"
+2 -2
View File
@@ -615,8 +615,8 @@ defmodule Regex do
defp safe_run(%Regex{re_pattern: re_pattern} = regex, string, options) do defp safe_run(%Regex{re_pattern: re_pattern} = regex, string, options) do
# TODO: Remove me when Erlang/OTP 28+ is required # TODO: Remove me when Erlang/OTP 28+ is required
# This allows regexes precompiled on Erlang/OTP 27- to work on Erlang/OTP 28+ # This allows regexes precompiled on Erlang/OTP 27- to work on Erlang/OTP 28+
with {:re_pattern, _, _, _, <<_::bitstring>>} <- re_pattern, with true <- :erlang.system_info(:otp_release) >= [?2, ?8],
true <- :erlang.system_info(:otp_release) >= [?2, ?8] do {:re_pattern, _, _, _, <<_::bitstring>>} <- re_pattern do
%Regex{source: source, opts: compile_opts} = regex %Regex{source: source, opts: compile_opts} = regex
:re.run(string, source, compile_opts ++ options) :re.run(string, source, compile_opts ++ options)
else else
+50 -88
View File
@@ -21,8 +21,8 @@ defmodule Registry do
the `:via` option), storing properties, custom dispatching rules, or a pubsub the `:via` option), storing properties, custom dispatching rules, or a pubsub
implementation. We explore some of those use cases below. implementation. We explore some of those use cases below.
The registry may also be transparently partitioned with the `:partitions` option, The registry may also be transparently partitioned, which provides
which provides more scalable behavior for running registries in highly concurrent more scalable behavior for running registries on highly concurrent
environments with thousands or millions of entries. environments with thousands or millions of entries.
## Using in `:via` ## Using in `:via`
@@ -79,7 +79,7 @@ defmodule Registry do
`Registry` has a dispatch mechanism that allows developers to implement custom `Registry` has a dispatch mechanism that allows developers to implement custom
dispatch logic triggered from the caller. For example, let's say we have a dispatch logic triggered from the caller. For example, let's say we have a
duplicate registry started as follows: duplicate registry started as so:
{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DispatcherTest) {:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DispatcherTest)
@@ -172,7 +172,7 @@ defmodule Registry do
the cost of delayed unsubscription. For example, if a process crashes, the cost of delayed unsubscription. For example, if a process crashes,
its keys are automatically removed from the registry but the change may its keys are automatically removed from the registry but the change may
not propagate immediately. This means certain operations may return processes not propagate immediately. This means certain operations may return processes
that are already dead. When this can happen, it will be explicitly stated that are already dead. When such may happen, it will be explicitly stated
in the function documentation. in the function documentation.
However, keep in mind those cases are typically not an issue. After all, a However, keep in mind those cases are typically not an issue. After all, a
@@ -218,7 +218,7 @@ defmodule Registry do
@typedoc "A list of guards to be evaluated when matching on objects in a registry" @typedoc "A list of guards to be evaluated when matching on objects in a registry"
@type guards :: [guard] @type guards :: [guard]
@typedoc "A pattern used to represent the output format part of a match spec" @typedoc "A pattern used to representing the output format part of a match spec"
@type body :: [term] @type body :: [term]
@typedoc "A full match spec used when selecting objects in the registry" @typedoc "A full match spec used when selecting objects in the registry"
@@ -230,7 +230,6 @@ defmodule Registry do
| {:name, registry} | {:name, registry}
| {:partitions, pos_integer} | {:partitions, pos_integer}
| {:listeners, [atom]} | {:listeners, [atom]}
| {:compressed, boolean}
| {:meta, [{meta_key, meta_value}]} | {:meta, [{meta_key, meta_value}]}
@typedoc """ @typedoc """
@@ -240,8 +239,8 @@ defmodule Registry do
""" """
@typedoc since: "1.15.0" @typedoc since: "1.15.0"
@type listener_message :: @type listener_message ::
{:register, registry, key, pid, value} {:register, registry, key, registry_partition :: pid, value}
| {:unregister, registry, key, pid} | {:unregister, registry, key, registry_partition :: pid}
@typedoc """ @typedoc """
Options used for `dispatch/4`. Options used for `dispatch/4`.
@@ -347,7 +346,7 @@ defmodule Registry do
The registry requires the following keys: The registry requires the following keys:
* `:keys` - controls whether keys are `:unique`, `:duplicate`, * `:keys` - chooses if keys are `:unique`, `:duplicate`,
`{:duplicate, :key}`, or `{:duplicate, :pid}` `{:duplicate, :key}`, or `{:duplicate, :pid}`
* `:name` - the name of the registry and its tables * `:name` - the name of the registry and its tables
@@ -358,8 +357,6 @@ defmodule Registry do
and unregister events. The registered process must be monitored by the and unregister events. The registered process must be monitored by the
listener if the listener wants to be notified if the registered process listener if the listener wants to be notified if the registered process
crashes. Messages sent to listeners are of type `t:listener_message/0`. crashes. Messages sent to listeners are of type `t:listener_message/0`.
* `:compressed` - turns on compression for registry's keys ETS tables, reducing
memory usage at the cost of additional CPU usage. Defaults to `false`.
* `:meta` - a keyword list of metadata to be attached to the registry. * `:meta` - a keyword list of metadata to be attached to the registry.
For `:duplicate` registries, you can specify the partitioning strategy For `:duplicate` registries, you can specify the partitioning strategy
@@ -639,7 +636,7 @@ defmodule Registry do
@doc """ @doc """
Finds the `{pid, value}` pair for the given `key` in `registry` in no particular order. Finds the `{pid, value}` pair for the given `key` in `registry` in no particular order.
Returns an empty list if there is no match. An empty list if there is no match.
For unique registries, a single partition lookup is necessary. For For unique registries, a single partition lookup is necessary. For
duplicate registries, all partitions must be looked up. duplicate registries, all partitions must be looked up.
@@ -737,7 +734,7 @@ defmodule Registry do
This function is useful only when spawning processes is not an option, This function is useful only when spawning processes is not an option,
for example, when copying the data to another process could be too for example, when copying the data to another process could be too
expensive, or when the work must be done within the current process expensive. Or when the work must be done within the current process
for other reasons. In such cases, this function provides a scalable for other reasons. In such cases, this function provides a scalable
mechanism for managing locks on top of the registry's infrastructure. mechanism for managing locks on top of the registry's infrastructure.
@@ -751,7 +748,6 @@ defmodule Registry do
""" """
@doc since: "1.18.0" @doc since: "1.18.0"
@spec lock(registry, term, (-> result)) :: result when result: var
def lock(registry, lock_key, function) def lock(registry, lock_key, function)
when is_atom(registry) and is_function(function, 0) do when is_atom(registry) and is_function(function, 0) do
{_kind, partitions, _, pid_ets, _} = info!(registry) {_kind, partitions, _, pid_ets, _} = info!(registry)
@@ -772,7 +768,7 @@ defmodule Registry do
For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple. For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple.
Please note that guard conditions will work only for assigned Please note that guard conditions will work only for assigned
variables like `:"$1"`, `:"$2"`, and so forth. variables like `:"$1"`, `:"$2"`, and so forth.
Avoid using of special match variables `:"$_"` and `:"$$"`, because they might not work as expected. Avoid usage of special match variables `:"$_"` and `:"$$"`, because it might not work as expected.
An empty list will be returned if there is no match. An empty list will be returned if there is no match.
@@ -842,7 +838,7 @@ defmodule Registry do
iex> Registry.keys(Registry.UniqueKeysTest, self()) iex> Registry.keys(Registry.UniqueKeysTest, self())
["hello"] ["hello"]
This is possible for duplicate registries though: Such is possible for duplicate registries though:
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateKeysTest) iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateKeysTest)
iex> Registry.keys(Registry.DuplicateKeysTest, self()) iex> Registry.keys(Registry.DuplicateKeysTest, self())
@@ -1131,26 +1127,22 @@ defmodule Registry do
end end
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
if exact_match_key?(key) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
else
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}} pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}], {[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]} [{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
else
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
end end
end end
defp unregister_match_specs(_kind, key, self, pattern, guards) do defp unregister_match_specs(_kind, key, self, pattern, guards) do
if exact_match_key?(key) do underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
{[{{key, {self, :_}}, [], [true]}], [{{key, {self, pattern}}, guards, [true]}]}
else
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
{[{{:_, {self, :_}}, [underscore_guard], [true]}], {[{{:_, {self, :_}}, [underscore_guard], [true]}],
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]} [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
end
end end
@doc """ @doc """
@@ -1464,12 +1456,12 @@ defmodule Registry do
end end
@doc """ @doc """
Selects key, pid, and values registered using full match specs. Select key, pid, and values registered using full match specs.
The `spec` consists of a list of three-part tuples, in the shape of `[{match_pattern, guards, body}]`. The `spec` consists of a list of three part tuples, in the shape of `[{match_pattern, guards, body}]`.
The first part, the match pattern, must be a tuple that will match the structure of the The first part, the match pattern, must be a tuple that will match the structure of the
data stored in the registry, which is `{key, pid, value}`. The atom `:_` can be used to the data stored in the registry, which is `{key, pid, value}`. The atom `:_` can be used to
ignore a given value or tuple element, while the atom `:"$1"` can be used to temporarily ignore a given value or tuple element, while the atom `:"$1"` can be used to temporarily
assign part of pattern to a variable for a subsequent comparison. This can be combined assign part of pattern to a variable for a subsequent comparison. This can be combined
like `{:"$1", :_, :_}`. like `{:"$1", :_, :_}`.
@@ -1481,7 +1473,7 @@ defmodule Registry do
variables like `:"$1"`, `:"$2"`, and so forth. variables like `:"$1"`, `:"$2"`, and so forth.
The third part, the body, is a list of shapes of the returned entries. Like guards, you have access to The third part, the body, is a list of shapes of the returned entries. Like guards, you have access to
assigned variables like `:"$1"`, which you can combine with hard-coded values to freely shape entries. assigned variables like `:"$1"`, which you can combine with hard-coded values to freely shape entries
Note that tuples have to be wrapped in an additional tuple. To get a result format like Note that tuples have to be wrapped in an additional tuple. To get a result format like
`%{key: key, pid: pid, value: value}`, assuming you bound those variables in order in the match part, `%{key: key, pid: pid, value: value}`, assuming you bound those variables in order in the match part,
you would provide a body like `[%{key: :"$1", pid: :"$2", value: :"$3"}]`. Like guards, you can use you would provide a body like `[%{key: :"$1", pid: :"$2", value: :"$3"}]`. Like guards, you can use
@@ -1492,9 +1484,8 @@ defmodule Registry do
that reference the underlying entry structure via `:"$_"` will return different results. that reference the underlying entry structure via `:"$_"` will return different results.
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead. Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
Note that for registries with many partitions, the result of each partition is concatenated. Note that for large registries with many partitions this will be costly as it builds the result by
Therefore selecting thousands of entries across partitions may become an expensive operation concatenating all the partitions.
and we recommend profiling/benchmarking whenever selecting too many entries.
## Examples ## Examples
@@ -1627,11 +1618,11 @@ defmodule Registry do
defp lookup_second({:duplicate, :key}, ets, key) do defp lookup_second({:duplicate, :key}, ets, key) do
spec = spec =
if exact_match_key?(key) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
else
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}] [{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
else
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
end end
try do try do
@@ -1673,24 +1664,25 @@ defmodule Registry do
def __unregister__(table, match, pos, ordered \\ false) def __unregister__(table, match, pos, ordered \\ false)
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
if exact_match_key?(key) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
:ets.match_delete(table, {{key, pid, :_}, :_})
else
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}} pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0 :ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
else
:ets.match_delete(table, {{key, pid, :_}, :_})
end end
end end
def __unregister__(table, match, pos, false = _ordered) do def __unregister__(table, match, pos, false = _ordered) do
key = :erlang.element(pos, match) key = :erlang.element(pos, match)
if exact_match_key?(key) do # We need to perform an element comparison if we have a special atom key.
:ets.match_delete(table, match) if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
else
match = :erlang.setelement(pos, match, :_) match = :erlang.setelement(pos, match, :_)
guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}} guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}}
:ets.select_delete(table, [{match, [guard], [true]}]) >= 0 :ets.select_delete(table, [{match, [guard], [true]}]) >= 0
else
:ets.match_delete(table, match)
end end
end end
@@ -1701,72 +1693,42 @@ defmodule Registry do
defp match_spec({:duplicate, :key}, key, pattern, guards) do defp match_spec({:duplicate, :key}, key, pattern, guards) do
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}] body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
if exact_match_key?(key) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
[{{{key, :_, :_}, pattern}, guards, body}]
else
guards = [ guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
] ]
[{{{:_, :_, :_}, pattern}, guards, body}] [{{{:_, :_, :_}, pattern}, guards, body}]
else
[{{{key, :_, :_}, pattern}, guards, body}]
end end
end end
defp match_spec(_kind, key, pattern, guards) do defp match_spec(_kind, key, pattern, guards) do
if exact_match_key?(key) do guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{key, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
else
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
end
end end
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
if exact_match_key?(key) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
[{{{key, :_, :_}, pattern}, guards, [true]}]
else
guards = [ guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
] ]
[{{{:_, :_, :_}, pattern}, guards, [true]}] [{{{:_, :_, :_}, pattern}, guards, [true]}]
else
[{{{key, :_, :_}, pattern}, guards, [true]}]
end end
end end
defp count_match_spec(_kind, key, pattern, guards) do defp count_match_spec(_kind, key, pattern, guards) do
if exact_match_key?(key) do guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{key, {:_, pattern}}, guards, [true]}] [{{:_, {:_, pattern}}, guards, [true]}]
else
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [true]}]
end
end end
# Only keys that remain exact when embedded in an ETS match head can use defp reserved_atom?("_"), do: true
# the optimized direct-key match path. defp reserved_atom?("$" <> _), do: true
defp exact_match_key?(term) when is_map(term), do: false defp reserved_atom?(_), do: false
defp exact_match_key?([]), do: true
defp exact_match_key?([head | tail]), do: exact_match_key?(head) and exact_match_key?(tail)
defp exact_match_key?(term) when is_tuple(term) do
exact_match_tuple?(term, tuple_size(term))
end
defp exact_match_key?(term) when is_atom(term) do
case Atom.to_string(term) do
"_" -> false
"$" <> _ -> false
_ -> true
end
end
defp exact_match_key?(_term), do: true
defp exact_match_tuple?(_term, 0), do: true
defp exact_match_tuple?(term, index) do
exact_match_key?(:erlang.element(index, term)) and exact_match_tuple?(term, index - 1)
end
defp ordered?({:duplicate, :key}), do: true defp ordered?({:duplicate, :key}), do: true
defp ordered?(_), do: false defp ordered?(_), do: false
@@ -1823,7 +1785,7 @@ defmodule Registry.Partition do
@key_info -2 @key_info -2
@doc """ @doc """
Returns the name of the key partition table. Returns the name of key partition table.
""" """
@spec key_name(atom, non_neg_integer) :: atom @spec key_name(atom, non_neg_integer) :: atom
def key_name(registry, partition) do def key_name(registry, partition) do
@@ -1831,7 +1793,7 @@ defmodule Registry.Partition do
end end
@doc """ @doc """
Returns the name of the pid partition table. Returns the name of pid partition table.
""" """
@spec pid_name(atom, non_neg_integer) :: atom @spec pid_name(atom, non_neg_integer) :: atom
def pid_name(name, partition) do def pid_name(name, partition) do
@@ -1848,7 +1810,7 @@ defmodule Registry.Partition do
end end
@doc """ @doc """
Runs the function with a lock. Runs function with a lock.
""" """
def lock(pid, key, lock) do def lock(pid, key, lock) do
ref = GenServer.call(pid, {:lock, key}) ref = GenServer.call(pid, {:lock, key})
+29 -104
View File
@@ -234,12 +234,12 @@ defmodule String do
to the definition of the encoding) is encountered, only one to the definition of the encoding) is encountered, only one
code point needs to be rejected. code point needs to be rejected.
Most functions in this module perform self-synchronization too. This module relies on this behavior to ignore such invalid
For example, `downcase/1` will return a downcased string, with characters. For example, `length/1` will return
any invalid codepoints preserved at their location. a correct result even if an invalid code point is fed into it.
In other words, this module expects invalid data to be detected In other words, this module expects invalid data to be detected
at the boundary, typically when retrieving data from the external source. elsewhere, usually when retrieving data from the external source.
For example, a driver that reads strings from a database will be For example, a driver that reads strings from a database will be
responsible to check the validity of the encoding. `String.chunk/2` responsible to check the validity of the encoding. `String.chunk/2`
can be used for breaking a string into valid and invalid parts. can be used for breaking a string into valid and invalid parts.
@@ -935,28 +935,18 @@ defmodule String do
end end
def upcase(string, :ascii) when is_binary(string) do def upcase(string, :ascii) when is_binary(string) do
upcase_ascii(string, string, 0) IO.iodata_to_binary(upcase_ascii(string))
end end
def upcase(string, mode) when is_binary(string) and mode in @conditional_mappings do def upcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
String.Unicode.upcase(string, [], mode) String.Unicode.upcase(string, [], mode)
end end
defp upcase_ascii(<<char, rest::bits>>, original, skip) when char >= ?a and char <= ?z do defp upcase_ascii(<<char, rest::bits>>) when char >= ?a and char <= ?z,
prefix = binary_part(original, 0, skip) do: [char - 32 | upcase_ascii(rest)]
upcase_ascii(rest, <<prefix::binary, char - 32>>)
end
defp upcase_ascii(<<_char, rest::bits>>, original, skip), defp upcase_ascii(<<char, rest::bits>>), do: [char | upcase_ascii(rest)]
do: upcase_ascii(rest, original, skip + 1) defp upcase_ascii(<<>>), do: []
defp upcase_ascii(<<>>, original, _skip), do: original
defp upcase_ascii(<<char, rest::bits>>, acc) when char >= ?a and char <= ?z,
do: upcase_ascii(rest, <<acc::binary, char - 32>>)
defp upcase_ascii(<<char, rest::bits>>, acc), do: upcase_ascii(rest, <<acc::binary, char>>)
defp upcase_ascii(<<>>, acc), do: acc
@doc """ @doc """
Converts all characters in the given string to lowercase according to `mode`. Converts all characters in the given string to lowercase according to `mode`.
@@ -1015,7 +1005,7 @@ defmodule String do
end end
def downcase(string, :ascii) when is_binary(string) do def downcase(string, :ascii) when is_binary(string) do
downcase_ascii(string, string, 0) IO.iodata_to_binary(downcase_ascii(string))
end end
def downcase(string, :greek) when is_binary(string) do def downcase(string, :greek) when is_binary(string) do
@@ -1026,21 +1016,11 @@ defmodule String do
String.Unicode.downcase(string, [], mode) String.Unicode.downcase(string, [], mode)
end end
defp downcase_ascii(<<char, rest::bits>>, original, skip) when char >= ?A and char <= ?Z do defp downcase_ascii(<<char, rest::bits>>) when char >= ?A and char <= ?Z,
prefix = binary_part(original, 0, skip) do: [char + 32 | downcase_ascii(rest)]
downcase_ascii(rest, <<prefix::binary, char + 32>>)
end
defp downcase_ascii(<<_char, rest::bits>>, original, skip), defp downcase_ascii(<<char, rest::bits>>), do: [char | downcase_ascii(rest)]
do: downcase_ascii(rest, original, skip + 1) defp downcase_ascii(<<>>), do: []
defp downcase_ascii(<<>>, original, _skip), do: original
defp downcase_ascii(<<char, rest::bits>>, acc) when char >= ?A and char <= ?Z,
do: downcase_ascii(rest, <<acc::binary, char + 32>>)
defp downcase_ascii(<<char, rest::bits>>, acc), do: downcase_ascii(rest, <<acc::binary, char>>)
defp downcase_ascii(<<>>, acc), do: acc
@doc """ @doc """
Converts the first character in the given string to Converts the first character in the given string to
@@ -1126,7 +1106,7 @@ defmodule String do
end end
@doc """ @doc """
Replaces all leading occurrences of `match` by `replacement` in `string`. Replaces all leading occurrences of `match` by `replacement` of `match` in `string`.
Returns the string untouched if there are no occurrences. Returns the string untouched if there are no occurrences.
@@ -1770,7 +1750,7 @@ defmodule String do
end end
defp do_replace(subject, [], _, n) do defp do_replace(subject, [], _, n) do
binary_part(subject, n, byte_size(subject) - n) [binary_part(subject, n, byte_size(subject) - n)]
end end
defp do_replace(subject, [{start, length} | matches], replacement, n) do defp do_replace(subject, [{start, length} | matches], replacement, n) do
@@ -1828,7 +1808,7 @@ defmodule String do
do: :unicode.characters_to_binary(acc) do: :unicode.characters_to_binary(acc)
defp do_reverse({:error, <<byte, rest::bits>>}, acc), defp do_reverse({:error, <<byte, rest::bits>>}, acc),
do: do_reverse(:unicode_util.gc(rest), []) <> <<byte>> <> :unicode.characters_to_binary(acc) do: :unicode.characters_to_binary(acc) <> <<byte>> <> do_reverse(:unicode_util.gc(rest), [])
@doc """ @doc """
Returns a string `subject` repeated `n` times. Returns a string `subject` repeated `n` times.
@@ -2454,7 +2434,7 @@ defmodule String do
"" ""
""" """
@spec slice(t, integer, non_neg_integer) :: t @spec slice(t, integer, non_neg_integer) :: grapheme
def slice(_, _, 0) do def slice(_, _, 0) do
"" ""
@@ -3006,38 +2986,29 @@ defmodule String do
end end
end end
# TODO: Deprecate String.to_atom, List.to_atom, and atom interpolation in v1.25
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
def to_atom(string) when is_binary(string) do
:erlang.binary_to_atom(string, :utf8)
end
@doc """ @doc """
Converts a string to an existing atom or creates a new one. Converts a string to an existing atom or creates a new one.
> #### Dynamic Atom Creation {: .warning} Warning: this function creates atoms dynamically and atoms are
> not garbage-collected. Therefore, `string` should not be an
> This function creates atoms dynamically and atoms are untrusted value, such as input received from a socket or during
> not garbage-collected. Therefore, `string` should not be an a web request. Consider using `to_existing_atom/1` instead.
> untrusted value, such as input received from a socket or during
> a web request. Consider using `to_existing_atom/1` instead.
By default, the maximum number of atoms is `1_048_576`. This limit By default, the maximum number of atoms is `1_048_576`. This limit
can be raised or lowered using the VM option `+t`. can be raised or lowered using the VM option `+t`.
The maximum atom size is 255 Unicode code points. The maximum atom size is of 255 Unicode code points.
Inlined by the compiler. Inlined by the compiler.
## Examples ## Examples
iex> String.to_unsafe_atom("my_atom") iex> String.to_atom("my_atom")
:my_atom :my_atom
""" """
@doc since: "1.21.0" @spec to_atom(String.t()) :: atom
@spec to_unsafe_atom(String.t()) :: atom def to_atom(string) when is_binary(string) do
def to_unsafe_atom(string) when is_binary(string) do
:erlang.binary_to_atom(string, :utf8) :erlang.binary_to_atom(string, :utf8)
end end
@@ -3045,9 +3016,7 @@ defmodule String do
Converts a string to an existing atom or raises if Converts a string to an existing atom or raises if
the atom does not exist. the atom does not exist.
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`. The maximum atom size is of 255 Unicode code points.
The maximum atom size is 255 Unicode code points.
Raises an `ArgumentError` if the atom does not exist. Raises an `ArgumentError` if the atom does not exist.
Inlined by the compiler. Inlined by the compiler.
@@ -3076,47 +3045,6 @@ defmodule String do
:erlang.binary_to_existing_atom(string, :utf8) :erlang.binary_to_existing_atom(string, :utf8)
end end
@doc """
Converts a string to one of the `allowed_atoms` or raises.
Raises an `ArgumentError` if the atom either does not exist or is not within
the existing list.
This should be preferred to `to_existing_atom/1` if the list is known upfront,
since there is no risk that the atom has not been loaded.
## Examples
iex> String.to_existing_atom("foo", [:foo, :bar])
:foo
iex> String.to_existing_atom("unknown", [:foo, :bar])
** (ArgumentError) unexpected value: \"unknown\", the allowed atoms are: [:foo, :bar]
"""
@doc since: "1.21.0"
@spec to_existing_atom(String.t(), nonempty_list(a)) :: a when a: atom()
def to_existing_atom(string, [_ | _] = allowed_atoms) when is_binary(string) do
atom = :erlang.binary_to_existing_atom(string, :utf8)
if atom not in allowed_atoms do
to_existing_atom_unexpected(string, allowed_atoms)
end
atom
end
# used just to have a less cryptic stacktrace and consistent error
@doc false
def __to_existing_atom__(string, allowed_atoms) do
to_existing_atom_unexpected(string, allowed_atoms)
end
defp to_existing_atom_unexpected(string, allowed_atoms) do
raise ArgumentError,
"unexpected value: #{inspect(string)}, the allowed atoms are: #{inspect(allowed_atoms)}"
end
@doc """ @doc """
Returns an integer whose text representation is `string`. Returns an integer whose text representation is `string`.
@@ -3214,13 +3142,10 @@ defmodule String do
0.75 0.75
iex> String.bag_distance("abcd", "abcd") iex> String.bag_distance("abcd", "abcd")
1.0 1.0
iex> String.bag_distance("", "")
1.0
""" """
@spec bag_distance(t, t) :: float @spec bag_distance(t, t) :: float
@doc since: "1.8.0" @doc since: "1.8.0"
def bag_distance("", ""), do: 1.0
def bag_distance(_string, ""), do: 0.0 def bag_distance(_string, ""), do: 0.0
def bag_distance("", _string), do: 0.0 def bag_distance("", _string), do: 0.0
@@ -3229,9 +3154,9 @@ defmodule String do
{bag2, length2} = string_to_bag(string2, %{}, 0) {bag2, length2} = string_to_bag(string2, %{}, 0)
diff1 = bag_difference(bag1, bag2) diff1 = bag_difference(bag1, bag2)
max_diff = if length1 >= length2, do: diff1, else: diff1 - length1 + length2 diff2 = bag_difference(bag2, bag1)
1 - max_diff / max(length1, length2) 1 - max(diff1, diff2) / max(length1, length2)
end end
defp string_to_bag(string, bag, length) do defp string_to_bag(string, bag, length) do
+1 -1
View File
@@ -88,7 +88,7 @@ defmodule StringIO do
If options are provided, the result will be `{:ok, pid}`, returning the If options are provided, the result will be `{:ok, pid}`, returning the
IO device created. The option `:capture_prompt`, when set to `true`, causes IO device created. The option `:capture_prompt`, when set to `true`, causes
prompts (which are specified as arguments to `IO.get*` functions) to be prompts (which are specified as arguments to `IO.get*` functions) to be
included in the device's output. See `open/3` for the list of supported included in the device's output. See `options/3` for the list of supported
options. options.
If a function is provided, the device will be created and sent to the If a function is provided, the device will be created and sent to the
+5 -16
View File
@@ -541,7 +541,7 @@ defmodule System do
By default, the Erlang VM register traps to the three By default, the Erlang VM register traps to the three
signals: signals:
* `:sigterm` - gracefully shuts down the VM with `stop/0` * `:sigstop` - gracefully shuts down the VM with `stop/0`
* `:sigquit` - halts the VM via `halt/0` * `:sigquit` - halts the VM via `halt/0`
* `:sigusr1` - halts the VM via status code of 1 * `:sigusr1` - halts the VM via status code of 1
@@ -1026,10 +1026,10 @@ defmodule System do
> On Unix systems, arguments are passed to a new operating system > On Unix systems, arguments are passed to a new operating system
> process as an array of strings but on Windows it is up to the child > process as an array of strings but on Windows it is up to the child
> process to parse them and some Windows programs may apply their own > process to parse them and some Windows programs may apply their own
> rules, which are inconsistent with the standard C runtime `argv` parsing. > rules, which are inconsistent with the standard C runtime `argv` parsing
> >
> This is particularly troublesome when invoking `.bat`, `.cmd`, or `.com` > This is particularly troublesome when invoking `.bat` or `.com` files
> files as these run implicitly through `cmd.exe`, whose argument parsing is > as these run implicitly through `cmd.exe`, whose argument parsing is
> vulnerable to malicious input and can be used to run arbitrary shell > vulnerable to malicious input and can be used to run arbitrary shell
> commands. > commands.
> >
@@ -1037,7 +1037,7 @@ defmodule System do
> files or `.com` applications, you must not pass untrusted input as > files or `.com` applications, you must not pass untrusted input as
> arguments to the program. You may avoid accidentally executing them > arguments to the program. You may avoid accidentally executing them
> by explicitly passing the extension of the program you want to run, > by explicitly passing the extension of the program you want to run,
> such as `.exe`, and double-checking the program is indeed not a batch > such as `.exe`, and double check the program is indeed not a batch
> file or `.com` application. > file or `.com` application.
## Options ## Options
@@ -1378,17 +1378,6 @@ defmodule System do
@doc """ @doc """
Returns the Erlang/OTP release number. Returns the Erlang/OTP release number.
The version is a single number, the major version of OTP.
Erlang/OTP does not provide an API to fetch the exact
patch version, see the [*Versions* page](https://www.erlang.org/doc/system/versions.html)
in the Erlang documentation for more information.
## Examples
iex> System.otp_release()
"#{:erlang.system_info(:otp_release)}"
""" """
@spec otp_release :: String.t() @spec otp_release :: String.t()
@doc since: "1.3.0" @doc since: "1.3.0"

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