Compare commits
140
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6050b8400a | ||
|
|
42a005e4df | ||
|
|
c2d39715ff | ||
|
|
472a49d84a | ||
|
|
a631da4fb0 | ||
|
|
7f0670e574 | ||
|
|
8bdd041122 | ||
|
|
8f264f3ced | ||
|
|
0eabf48524 | ||
|
|
b0db1a26e1 | ||
|
|
35eeab3511 | ||
|
|
aae39c87dc | ||
|
|
8a2c815c85 | ||
|
|
0b9b5b962b | ||
|
|
41353c6cf8 | ||
|
|
7e49585d26 | ||
|
|
7081e7223e | ||
|
|
3e3ce13c1d | ||
|
|
4816d3773a | ||
|
|
5b8b8e3589 | ||
|
|
aafc248179 | ||
|
|
b8723fea1e | ||
|
|
9412f483d7 | ||
|
|
8a35ffa811 | ||
|
|
bb21320928 | ||
|
|
a5435a6ead | ||
|
|
35497755b5 | ||
|
|
c3f7e3473e | ||
|
|
68c413f76c | ||
|
|
16f5174580 | ||
|
|
1254dae84f | ||
|
|
651bdb485d | ||
|
|
46ebf7cc83 | ||
|
|
abf9ba1ae2 | ||
|
|
46811eccc9 | ||
|
|
9199e5595c | ||
|
|
b8a29e0c1a | ||
|
|
ab5b33d8ac | ||
|
|
c376c0feb6 | ||
|
|
5858e7c470 | ||
|
|
2fd350672b | ||
|
|
002e382794 | ||
|
|
46841a22e4 | ||
|
|
bdf8e4481d | ||
|
|
3120d4539b | ||
|
|
facde14525 | ||
|
|
40e930607a | ||
|
|
03c123a149 | ||
|
|
81975a2de1 | ||
|
|
133dfa8d46 | ||
|
|
39d646f459 | ||
|
|
ad262dc8e4 | ||
|
|
a0e1604fe6 | ||
|
|
11f7d8f84a | ||
|
|
f4e1b34617 | ||
|
|
55a3899e75 | ||
|
|
ada2817d72 | ||
|
|
89e0106afc | ||
|
|
e39a1ca796 | ||
|
|
007efde2d5 | ||
|
|
eb8bbc9c29 | ||
|
|
c5332235ab | ||
|
|
e5d8b00e5b | ||
|
|
2ba06cc40b | ||
|
|
68aae43703 | ||
|
|
2019de2d82 | ||
|
|
d02e61d75e | ||
|
|
f2f2da9b40 | ||
|
|
13d412864c | ||
|
|
76d3fbad88 | ||
|
|
b8a9dc60a5 | ||
|
|
870d28fe64 | ||
|
|
2d6e61c4f5 | ||
|
|
81dc36568b | ||
|
|
99e0fe50a3 | ||
|
|
8a4915008d | ||
|
|
0fe5b13320 | ||
|
|
808075d87a | ||
|
|
c14f3c7acb | ||
|
|
5e7190c39f | ||
|
|
4ad7497d0e | ||
|
|
25cbbfe5d2 | ||
|
|
a5e19ad385 | ||
|
|
983e033863 | ||
|
|
890d9bbf00 | ||
|
|
20fef8cd1c | ||
|
|
9231515db8 | ||
|
|
d5e032efd1 | ||
|
|
5e44c78fff | ||
|
|
4d56bdd898 | ||
|
|
2e9981d98c | ||
|
|
ed478e816b | ||
|
|
370e7db14a | ||
|
|
d67cb9634b | ||
|
|
a37db60f78 | ||
|
|
116f8f472f | ||
|
|
9c1bd76b26 | ||
|
|
3cb0b51836 | ||
|
|
5b97e9c132 | ||
|
|
8ba8ad709f | ||
|
|
37dc125f2a | ||
|
|
9fb8fb1216 | ||
|
|
5a1fa351dc | ||
|
|
20cd669a0d | ||
|
|
03b45e521d | ||
|
|
bcfefa18ed | ||
|
|
b7a832ea32 | ||
|
|
8011552eac | ||
|
|
ebb7c73353 | ||
|
|
898f1a2f69 | ||
|
|
603602e67b | ||
|
|
92221937d3 | ||
|
|
245066b405 | ||
|
|
4d30464ec4 | ||
|
|
139da8fa5f | ||
|
|
8fdef5c48f | ||
|
|
d0514dfd15 | ||
|
|
325608b518 | ||
|
|
c4275e39b2 | ||
|
|
1bf7bb0682 | ||
|
|
7d5d1e27ca | ||
|
|
50cc7ef182 | ||
|
|
5ec106a5e2 | ||
|
|
5c845c796c | ||
|
|
469acb110f | ||
|
|
7061c37b88 | ||
|
|
74bb90a3cd | ||
|
|
5d794ab17c | ||
|
|
c1256f161c | ||
|
|
8f40bf117a | ||
|
|
8d691697ff | ||
|
|
6f2e24ca63 | ||
|
|
8cc693864a | ||
|
|
0f8baa3041 | ||
|
|
017162a975 | ||
|
|
f74b3648e2 | ||
|
|
28b6616a59 | ||
|
|
7c2d4f3d8e | ||
|
|
ba4eb73734 | ||
|
|
fbece98a5d |
@@ -26,12 +26,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "29.0-rc1"
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "29.0"
|
||||
deterministic: true
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "28.4"
|
||||
docs: true
|
||||
coverage: true
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "27.3"
|
||||
- otp_version: "27.0"
|
||||
- otp_version: master
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
- name: "Upload Coverage Artifact"
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: TestCoverage
|
||||
path: cover/*
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version:
|
||||
- "29.0-rc1"
|
||||
- "29.0"
|
||||
- "28.1"
|
||||
- "27.3"
|
||||
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -52,4 +52,4 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
@@ -38,4 +38,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
build_docs: build_docs
|
||||
|
||||
- otp: 29
|
||||
otp_version: "29.0-rc1"
|
||||
otp_version: "29.0"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -100,19 +100,19 @@ jobs:
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.zip
|
||||
|
||||
- name: "Upload Windows release artifacts"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.exe
|
||||
|
||||
- name: "Upload doc artifacts"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: matrix.build_docs
|
||||
with:
|
||||
name: Docs
|
||||
@@ -139,20 +139,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Download build"
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
|
||||
- name: Log in to Azure
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: "Sign files with Trusted Signing"
|
||||
uses: azure/trusted-signing-action@87c2e83e6868da99d3380aa309851b32ed9a8346 # v1.1.0
|
||||
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
|
||||
- name: "Download Build Artifacts"
|
||||
id: download-build-artifacts
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
|
||||
- name: Attest Distribution Assets with SBoM
|
||||
id: attest-sbom
|
||||
uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4.0.0
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
||||
|
||||
- name: "Assemble Release SBoM Artifacts"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "SBoM"
|
||||
path: |
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
||||
|
||||
- name: "Assemble Distribution Attestations"
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "Attestations"
|
||||
path: "attestations/*.sigstore"
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
||||
merge-multiple: true
|
||||
@@ -333,7 +333,7 @@ jobs:
|
||||
echo "Required variables for uploading to hex.pm are not set up, skipping..."
|
||||
exit 1
|
||||
|
||||
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
|
||||
+66
-10
@@ -175,6 +175,68 @@ The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
|
||||
|
||||
## v1.20.0-rc.5 (2026-05-13)
|
||||
|
||||
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
|
||||
* [Float] Optimize `Float.round/2` by avoiding big integers
|
||||
* [Inspect] Increase inspect limit to help print deeply nested data structures
|
||||
* [Inspect] Support printing Erlang records (using Erlang notation)
|
||||
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
|
||||
* [Registry] Switch `{:duplicate, :key}` key_ets to ordered_set with composite keys
|
||||
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Add `source/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix app.tree] Support `--output` option
|
||||
* [mix deps.tree] Support `--output` option
|
||||
* [mix help] Support printing docs for types and callbacks
|
||||
* [mix format] Support `--no-compile` option
|
||||
* [mix source] Add `mix source MODULE` to print or open a given module/function location
|
||||
|
||||
### 2. Potential breaking changes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Disallow raw CR line ending in strings, comments and after `?` for security reasons
|
||||
|
||||
### 3. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
|
||||
* [Kernel] Reject negative Duration in `to_timeout/1`
|
||||
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
|
||||
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
|
||||
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
|
||||
* [String] Support empty pattern list in `String.count/2`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Persist log level to app env in `Logger.configure/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
|
||||
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
|
||||
|
||||
## v1.20.0-rc.4 (2026-03-31)
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
@@ -187,7 +249,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
|
||||
* [File] Add support for `[:raw]` opts in `File.read/2`
|
||||
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
|
||||
* [Module] Purge and delete modules if `after_verify/2` callback fails
|
||||
* [Module] Purge and delete modules if `after_compile/2` callback fails
|
||||
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
|
||||
* [Process] Add `Process.get_label/1`
|
||||
|
||||
@@ -232,7 +294,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
#### Elixir
|
||||
|
||||
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
|
||||
* [File] Preserve directory permissions in `File.cp_r/3`
|
||||
* [File] Allowing preserving directory permissions in `File.cp_r/3`
|
||||
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
|
||||
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
|
||||
* [File] Warn when defining `@type record()`, fixes CI on Erlang/OTP 29
|
||||
@@ -261,13 +323,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
|
||||
|
||||
### 2. Potential breaking changes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* `map.foo()` (accessing a map field with parens) and `mod.foo` (invoking a function without parens) will now raise instead of emitting runtime warnings, aligning themselves with the type system behaviour
|
||||
|
||||
### 3. Bug fixes
|
||||
### 2. Bug fixes
|
||||
|
||||
#### IEx
|
||||
|
||||
@@ -318,7 +374,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
#### Elixir
|
||||
|
||||
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note that while Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
|
||||
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
|
||||
|
||||
### 3. Hard deprecations
|
||||
|
||||
|
||||
+68
-25
@@ -99,15 +99,11 @@ case you are looking for some examples:
|
||||
Once a pull request is sent, the Elixir team will review your changes.
|
||||
We outline our process below to clarify the roles of everyone involved.
|
||||
|
||||
All pull requests must be approved by two committers before being merged into
|
||||
the repository. If changes are necessary, the team will leave appropriate
|
||||
comments requesting changes to the code. Unfortunately, we cannot guarantee a
|
||||
pull request will be merged, even when modifications are requested, as the Elixir
|
||||
team will re-evaluate the contribution as it changes.
|
||||
|
||||
Committers may also push style changes directly to your branch. If you would
|
||||
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
|
||||
feature when submitting your pull request.
|
||||
All pull requests must be reviewed before being merged into the repository.
|
||||
If changes are necessary, the team will leave appropriate comments requesting
|
||||
changes to the code. Unfortunately, we cannot guarantee a pull request will
|
||||
be merged, even when modifications are requested, as the Elixir team will
|
||||
re-evaluate the contribution as it changes.
|
||||
|
||||
The Elixir team may optionally assign someone to review a pull request.
|
||||
If someone is assigned, they must explicitly approve the code before
|
||||
@@ -118,6 +114,29 @@ into the repository. If you have carefully organized your commits and
|
||||
believe they should be merged without squashing, please mention it in
|
||||
a comment.
|
||||
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
|
||||
```sh
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
|
||||
# and generate HTML and EPUB documents:
|
||||
make docs
|
||||
```
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
|
||||
## Licensing and Compliance Requirements
|
||||
|
||||
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
|
||||
@@ -181,25 +200,49 @@ affecting **all external contributors**:
|
||||
See <https://developercertificate.org/> for a copy of the Developer Certificate
|
||||
of Origin license.
|
||||
|
||||
## Building documentation
|
||||
## Using AI and coding agents
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
While we allow the use of AI on contributions and discussions, please be mindful
|
||||
when doing so. Generally speaking, Elixir maintainers already have access to AI
|
||||
(like many other developers). Therefore, if we need the feedback or help of a
|
||||
coding agent, we can request so ourselves. For this reason, we often find
|
||||
the point of view of the human behind the agent more valuable.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
That said, here are examples of how one might (or might not) use AI and coding
|
||||
agents in Elixir spaces:
|
||||
|
||||
```sh
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
* When it comes to discussions, using AI to help express yourself is welcome,
|
||||
but avoid directly copy and pasting AI generated content. If there is a language
|
||||
barrier, use AI to translate, review, and improve your text, but do not use AI
|
||||
to respond on your behalf.
|
||||
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
* Do not use coding agents to tackle existing issues unless they have the
|
||||
"Contributions Welcome" label.
|
||||
|
||||
# and generate HTML and EPUB documents:
|
||||
make docs
|
||||
```
|
||||
* If you request a feature on the mailing list and it is accepted, you may
|
||||
use coding agents to implement it, as long as it follows the AI Contributions
|
||||
guidelines below.
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
* When automating AI usage on the Elixir codebase for performance improvements
|
||||
or security fixes, pair it with a separate set of agents whose job is to argue
|
||||
against and try to invalidate any proposed change. And treat their approval as
|
||||
advisory: a human must still validate it before opening issues or pull requests.
|
||||
|
||||
If any code is written by AI, then you must follow the guidelines below.
|
||||
|
||||
### AI contributions
|
||||
|
||||
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
|
||||
Certificate of Origin (DCO). The human submitter is responsible for:
|
||||
|
||||
* Reviewing all AI-generated code
|
||||
* Ensuring compliance with licensing requirements
|
||||
* Adding their own Signed-off-by tag to certify the DCO
|
||||
* Taking full responsibility for the contribution
|
||||
* Disclosing use of AI for comments and code contributions
|
||||
|
||||
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
|
||||
AI in the development process. Contributions should include an Assisted-by tag in the
|
||||
following format:
|
||||
|
||||
Assisted-by: AGENT_NAME:MODEL_VERSION
|
||||
|
||||
@@ -122,9 +122,10 @@ variable `ERL_COMPILER_OPTIONS=deterministic`.
|
||||
Contributions to Elixir are always welcome! Before you get started, please check
|
||||
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
|
||||
guidelines on how to set up your environment, run the test suite, format your
|
||||
code, and submit pull requests. We also include information on our review
|
||||
process, licensing requirements, and helpful tips to ensure a smooth
|
||||
contribution experience.
|
||||
code, and submit pull requests.
|
||||
|
||||
Note you must disclose the use of coding agents and AI written code in your
|
||||
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Development links
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.0-rc.4
|
||||
ELIXIR_VERSION=1.20.0-rc.5
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.20.0-rc.4
|
||||
set ELIXIR_VERSION=1.20.0-rc.5
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+25
-12
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
|
||||
file: file,
|
||||
source: source,
|
||||
line: line,
|
||||
quoted: [],
|
||||
quoted: %{},
|
||||
parser_options: [indentation: indentation] ++ parser_options,
|
||||
indentation: indentation
|
||||
}
|
||||
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
|
||||
state.parser_options
|
||||
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
buffer = handle_expr(buffer, mark, expr, meta, state)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -366,7 +366,7 @@ defmodule EEx.Compiler do
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[{contents, start_line, start_column} | scope],
|
||||
%{state | quoted: [], line: line}
|
||||
%{state | quoted: %{}, line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
@@ -376,7 +376,7 @@ defmodule EEx.Compiler do
|
||||
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
buffer = handle_expr(buffer, mark, contents, meta, state)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -410,7 +410,7 @@ defmodule EEx.Compiler do
|
||||
) do
|
||||
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
|
||||
options = [file: state.file, line: line, column: column] ++ state.parser_options
|
||||
tuples = Code.string_to_quoted!(wrapped, options)
|
||||
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, rest}
|
||||
end
|
||||
@@ -426,7 +426,7 @@ defmodule EEx.Compiler do
|
||||
|
||||
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
|
||||
message = "expected a closing '<% end %>' for block expression in EEx"
|
||||
expr_meta = non_whitespace_meta(content, line, column, state)
|
||||
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
|
||||
syntax_error!(message, expr_meta, state)
|
||||
end
|
||||
|
||||
@@ -443,10 +443,10 @@ defmodule EEx.Compiler do
|
||||
|
||||
defp wrap_expr(current, line, buffer, chars, state) do
|
||||
new_lines = List.duplicate(?\n, line - state.line)
|
||||
key = length(state.quoted)
|
||||
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
|
||||
count = current ++ placeholder ++ new_lines ++ chars
|
||||
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
|
||||
key = map_size(state.quoted)
|
||||
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
|
||||
count = [current, placeholder, new_lines, chars]
|
||||
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
|
||||
|
||||
{count, new_state}
|
||||
end
|
||||
@@ -479,8 +479,7 @@ defmodule EEx.Compiler do
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
{^key, value} = List.keyfind(quoted, key, 0)
|
||||
value
|
||||
Map.fetch!(quoted, key)
|
||||
end
|
||||
|
||||
defp insert_quoted({left, line, right}, quoted) do
|
||||
@@ -513,6 +512,20 @@ defmodule EEx.Compiler do
|
||||
column: meta.column
|
||||
end
|
||||
|
||||
defp handle_expr(buffer, mark, expr, meta, state) do
|
||||
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
rescue
|
||||
e in EEx.SyntaxError ->
|
||||
reraise %{
|
||||
e
|
||||
| file: e.file || state.file,
|
||||
line: e.line || meta.line,
|
||||
column: e.column || meta.column,
|
||||
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
|
||||
},
|
||||
__STACKTRACE__
|
||||
end
|
||||
|
||||
defp code_snippet(source, indentation, meta) do
|
||||
line_start = max(meta.line - 3, 1)
|
||||
line_end = meta.line
|
||||
|
||||
@@ -575,6 +575,18 @@ defmodule EExTest do
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
end
|
||||
|
||||
test "unsupported marker error carries template location metadata" do
|
||||
error =
|
||||
assert_raise EEx.SyntaxError, fn ->
|
||||
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
|
||||
end
|
||||
|
||||
assert error.file == "sample.eex"
|
||||
assert error.line == 7
|
||||
assert error.column == 1
|
||||
assert Exception.message(error) =~ "sample.eex:7:1:"
|
||||
end
|
||||
end
|
||||
|
||||
describe "warnings" do
|
||||
|
||||
@@ -1091,6 +1091,11 @@ defmodule Access do
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
|
||||
when index > last do
|
||||
{:lists.reverse(gets), :lists.reverse(updates, rest)}
|
||||
end
|
||||
|
||||
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||
if index in range do
|
||||
case next.(head) do
|
||||
|
||||
+278
-54
@@ -155,6 +155,255 @@ defmodule Base do
|
||||
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
|
||||
end
|
||||
|
||||
# SWAR (SIMD Within A Register) fast paths for valid16?/2 and valid32?/2
|
||||
# (non-hex). Each chunk of 8 bytes is validated in one guard: 7 bytes via
|
||||
# bitwise arithmetic on a single 56-bit integer, plus a per-byte range
|
||||
# check for the 8th byte. 56 bits is the largest width that fits in a BEAM
|
||||
# small int on 64-bit (fixnum range is 59-bit signed); at 64 bits every
|
||||
# `w + 0x80..` would allocate a bignum on the heap and the optimisation
|
||||
# would collapse. See https://github.com/erlang/otp/pull/10938 for the
|
||||
# corresponding pattern in OTP.
|
||||
@swar_mask80 0x80808080808080
|
||||
|
||||
# Per-range SWAR constants, broadcast across 7 lanes. Naming convention:
|
||||
# @swar_ge_X = 0x80 - X → high bit of `(w + @swar_ge_X)` lane is set
|
||||
# iff that byte is ≥ X
|
||||
# @swar_gt_X = 0x7F - X → high bit of `(w + @swar_gt_X)` lane is set
|
||||
# iff that byte is > X
|
||||
# A byte is in range [lo, hi] iff
|
||||
# `bxor(w + @swar_ge_lo, w + @swar_gt_hi)` has its high bit set.
|
||||
@swar_ge_0 0x50505050505050
|
||||
@swar_gt_9 0x46464646464646
|
||||
@swar_ge_2 0x4E4E4E4E4E4E4E
|
||||
@swar_gt_7 0x48484848484848
|
||||
@swar_ge_A 0x3F3F3F3F3F3F3F
|
||||
@swar_gt_F 0x39393939393939
|
||||
@swar_gt_V 0x29292929292929
|
||||
@swar_gt_Z 0x25252525252525
|
||||
@swar_ge_a 0x1F1F1F1F1F1F1F
|
||||
@swar_gt_f 0x19191919191919
|
||||
@swar_gt_v 0x09090909090909
|
||||
@swar_gt_z 0x05050505050505
|
||||
|
||||
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
|
||||
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
|
||||
# check — saves one Mycroft singleton. Trick lifted from
|
||||
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
|
||||
@swar_ge_slash 0x51515151515151
|
||||
|
||||
# Mycroft zero-byte detection for base64 singletons (+, -, _).
|
||||
# Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit
|
||||
# set, i.e. that byte's V value was 0 → original byte was K. Simplified
|
||||
# (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false
|
||||
# positives only occur for adjacent bytes that happen to equal `K xor 0x01`,
|
||||
# which is outside the base64 alphabet, so it never matters here.
|
||||
# Pattern follows https://github.com/elixir-lang/elixir/pull/15255.
|
||||
@swar_mask01 0x01010101010101
|
||||
@swar_plus_x7 0x2B2B2B2B2B2B2B
|
||||
@swar_dash_x7 0x2D2D2D2D2D2D2D
|
||||
@swar_under_x7 0x5F5F5F5F5F5F5F
|
||||
|
||||
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
|
||||
# of each stride and in the body of the sub-8-byte tail clauses).
|
||||
@compile {:inline,
|
||||
valid_char16upper?: 1,
|
||||
valid_char16lower?: 1,
|
||||
valid_char16mixed?: 1,
|
||||
valid_char32upper?: 1,
|
||||
valid_char32lower?: 1,
|
||||
valid_char32mixed?: 1,
|
||||
valid_char32hexupper?: 1,
|
||||
valid_char32hexlower?: 1,
|
||||
valid_char32hexmixed?: 1,
|
||||
valid_char64base?: 1,
|
||||
valid_char64url?: 1,
|
||||
valid_word16upper?: 1,
|
||||
valid_word16lower?: 1,
|
||||
valid_word16mixed?: 1,
|
||||
valid_word32upper?: 1,
|
||||
valid_word32lower?: 1,
|
||||
valid_word32mixed?: 1,
|
||||
valid_word32hexupper?: 1,
|
||||
valid_word32hexlower?: 1,
|
||||
valid_word32hexmixed?: 1,
|
||||
valid_word64base?: 1,
|
||||
valid_word64url?: 1}
|
||||
|
||||
defp valid_char16upper?(c), do: c in ?0..?9 or c in ?A..?F
|
||||
defp valid_char16lower?(c), do: c in ?0..?9 or c in ?a..?f
|
||||
defp valid_char16mixed?(c), do: c in ?0..?9 or c in ?A..?F or c in ?a..?f
|
||||
|
||||
defp valid_char32upper?(c), do: c in ?A..?Z or c in ?2..?7
|
||||
defp valid_char32lower?(c), do: c in ?a..?z or c in ?2..?7
|
||||
defp valid_char32mixed?(c), do: c in ?A..?Z or c in ?a..?z or c in ?2..?7
|
||||
|
||||
# Most common range first — letters dominate (22/32) over digits (10/32)
|
||||
# in hex base32, so letters go first in the OR short-circuit.
|
||||
defp valid_char32hexupper?(c), do: c in ?A..?V or c in ?0..?9
|
||||
defp valid_char32hexlower?(c), do: c in ?a..?v or c in ?0..?9
|
||||
defp valid_char32hexmixed?(c), do: c in ?A..?V or c in ?a..?v or c in ?0..?9
|
||||
|
||||
defp valid_char64base?(c),
|
||||
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?+ or c == ?/
|
||||
|
||||
defp valid_char64url?(c),
|
||||
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?- or c == ?_
|
||||
|
||||
# SWAR 7-byte word validity. Structure for each function:
|
||||
# 1. ASCII gate `band(w, MASK80) == 0` — every byte < 0x80 so the
|
||||
# additions below cannot carry across lanes.
|
||||
# 2. "Each byte is in range A OR range B (OR range C)" gate — OR per-
|
||||
# range XOR masks (high bit set in lane iff byte in that range), AND
|
||||
# with MASK80, demand all 7 high bits set.
|
||||
defp valid_word16upper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_F)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word16lower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_f)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word16mixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_F)
|
||||
),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_f)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32upper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32lower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32mixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexupper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_V)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexlower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_v)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexmixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_V)
|
||||
),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_v)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
|
||||
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
|
||||
# part of one range (Lemire merge), leaving only '+' as a Mycroft singleton.
|
||||
# For url, the singletons '-' and '_' are detected via two Mycroft terms.
|
||||
defp valid_word64base?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_slash, w + @swar_gt_9)
|
||||
),
|
||||
bxor(w, @swar_plus_x7) - @swar_mask01
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word64url?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9)
|
||||
),
|
||||
bor(
|
||||
bxor(w, @swar_dash_x7) - @swar_mask01,
|
||||
bxor(w, @swar_under_x7) - @swar_mask01
|
||||
)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 16 encoded string.
|
||||
|
||||
@@ -371,25 +620,21 @@ defmodule Base do
|
||||
decode_name = :"decode16#{base}!"
|
||||
validate_name = :"validate16#{base}?"
|
||||
valid_char_name = :"valid_char16#{base}?"
|
||||
valid_word_name = :"valid_word16#{base}?"
|
||||
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated entirely via
|
||||
# `valid_word16<base>?` in the body. The `and` short-circuits when SWAR
|
||||
# fails on any byte. Tail bytes (1-6 leftover) recurse through the
|
||||
# single-byte clause below.
|
||||
defp unquote(validate_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_name)(rest)
|
||||
|
||||
defp unquote(validate_name)(<<>>), do: true
|
||||
|
||||
defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
|
||||
unquote(valid_char_name)(c1) and
|
||||
unquote(valid_char_name)(c2) and
|
||||
unquote(validate_name)(rest)
|
||||
end
|
||||
|
||||
defp unquote(validate_name)(<<_char, _rest::binary>>), do: false
|
||||
|
||||
@compile {:inline, [{valid_char_name, 1}]}
|
||||
defp unquote(valid_char_name)(char)
|
||||
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
|
||||
do: true
|
||||
|
||||
defp unquote(valid_char_name)(_char), do: false
|
||||
defp unquote(validate_name)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
|
||||
|
||||
defp unquote(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
@@ -761,23 +1006,19 @@ defmodule Base do
|
||||
validate_name = :"validate64#{base}?"
|
||||
validate_main_name = :"validate_main64#{validate_name}?"
|
||||
valid_char_name = :"valid_char64#{base}?"
|
||||
valid_word_name = :"valid_word64#{base}?"
|
||||
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
|
||||
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
|
||||
# 8-byte-multiple `main`) recurses through the single-byte clause.
|
||||
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_main_name)(<<>>), do: true
|
||||
|
||||
defp unquote(validate_main_name)(
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
|
||||
) do
|
||||
unquote(valid_char_name)(c1) and
|
||||
unquote(valid_char_name)(c2) and
|
||||
unquote(valid_char_name)(c3) and
|
||||
unquote(valid_char_name)(c4) and
|
||||
unquote(valid_char_name)(c5) and
|
||||
unquote(valid_char_name)(c6) and
|
||||
unquote(valid_char_name)(c7) and
|
||||
unquote(valid_char_name)(c8) and
|
||||
unquote(validate_main_name)(rest)
|
||||
end
|
||||
defp unquote(validate_main_name)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_name)(<<>>, _pad?), do: true
|
||||
|
||||
@@ -863,13 +1104,6 @@ defmodule Base do
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, [{valid_char_name, 1}]}
|
||||
defp unquote(valid_char_name)(char)
|
||||
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
|
||||
do: true
|
||||
|
||||
defp unquote(valid_char_name)(_char), do: false
|
||||
|
||||
defp unquote(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
|
||||
@@ -1425,21 +1659,18 @@ defmodule Base do
|
||||
valid_char_name = :"valid_char32#{base}?"
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
|
||||
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
|
||||
# 8-byte-multiple `main`) recurses through the single-byte clause.
|
||||
valid_word_name = :"valid_word32#{base}?"
|
||||
|
||||
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_main_name)(<<>>), do: true
|
||||
|
||||
defp unquote(validate_main_name)(
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
|
||||
) do
|
||||
unquote(valid_char_name)(c1) and
|
||||
unquote(valid_char_name)(c2) and
|
||||
unquote(valid_char_name)(c3) and
|
||||
unquote(valid_char_name)(c4) and
|
||||
unquote(valid_char_name)(c5) and
|
||||
unquote(valid_char_name)(c6) and
|
||||
unquote(valid_char_name)(c7) and
|
||||
unquote(valid_char_name)(c8) and
|
||||
unquote(validate_main_name)(rest)
|
||||
end
|
||||
defp unquote(validate_main_name)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_name)(<<>>, _pad?), do: true
|
||||
|
||||
@@ -1519,13 +1750,6 @@ defmodule Base do
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, [{valid_char_name, 1}]}
|
||||
defp unquote(valid_char_name)(char)
|
||||
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
|
||||
do: true
|
||||
|
||||
defp unquote(valid_char_name)(_char), do: false
|
||||
|
||||
defp unquote(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
|
||||
|
||||
@@ -671,7 +671,7 @@ defmodule Calendar.ISO do
|
||||
{{year, month, day}, time_from_day_fraction(day_fraction)}
|
||||
|
||||
{extra_days, day_fraction} ->
|
||||
base_days = date_to_iso_days(year, month, day)
|
||||
base_days = valid_date_to_iso_days(year, month, day)
|
||||
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
|
||||
end
|
||||
|
||||
@@ -892,12 +892,15 @@ defmodule Calendar.ISO do
|
||||
|
||||
# Converts year, month, day to count of days since 0000-01-01.
|
||||
@doc false
|
||||
def date_to_iso_days(0, 1, 1), do: 0
|
||||
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
end
|
||||
|
||||
defp valid_date_to_iso_days(0, 1, 1), do: 0
|
||||
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
defp valid_date_to_iso_days(year, month, day) do
|
||||
y = if month <= 2, do: year - 1, else: year
|
||||
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
|
||||
year_of_era = y - era * @years_per_era
|
||||
@@ -1875,7 +1878,7 @@ defmodule Calendar.ISO do
|
||||
@doc false
|
||||
def shift_days({year, month, day}, days) do
|
||||
{year, month, day} =
|
||||
date_to_iso_days(year, month, day)
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
|> Kernel.+(days)
|
||||
|> date_from_iso_days()
|
||||
|
||||
|
||||
@@ -715,16 +715,18 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec to_date(Calendar.naive_datetime()) :: Date.t()
|
||||
def to_date(%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
}) do
|
||||
def to_date(
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
} = _naive_datetime
|
||||
) do
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
@@ -741,16 +743,18 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec to_time(Calendar.naive_datetime()) :: Time.t()
|
||||
def to_time(%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}) do
|
||||
def to_time(
|
||||
%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = _naive_datetime
|
||||
) do
|
||||
%Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
@@ -1146,16 +1150,18 @@ defmodule NaiveDateTime do
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
|
||||
def to_gregorian_seconds(%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}) do
|
||||
def to_gregorian_seconds(
|
||||
%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
} = _naive_datetime
|
||||
) do
|
||||
{days, day_fraction} =
|
||||
calendar.naive_datetime_to_iso_days(
|
||||
year,
|
||||
|
||||
@@ -286,7 +286,7 @@ defmodule Code do
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
static_atoms_encoder: (atom() -> term()),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
|
||||
@@ -1319,7 +1319,7 @@ defmodule Code do
|
||||
and keyword lists.
|
||||
|
||||
The encoder function will receive the atom name (as a binary) and a
|
||||
keyword list with the current file, line and column. It must return
|
||||
keyword list with the current line and column. It must return
|
||||
`{:ok, token :: term} | {:error, reason :: binary}`.
|
||||
|
||||
The encoder function is supposed to create an atom from the given
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Bit containers
|
||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_bitstring(quoted, state)
|
||||
normalize_bitstring(quoted, state, false)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
@@ -89,13 +89,7 @@ defmodule Code.Normalizer do
|
||||
normalize_literal(:utf8, [], state)
|
||||
end
|
||||
|
||||
string =
|
||||
if state.escape do
|
||||
normalize_bitstring(string, state, true)
|
||||
else
|
||||
normalize_bitstring(string, state)
|
||||
end
|
||||
|
||||
string = normalize_bitstring(string, state, state.escape)
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||
end
|
||||
|
||||
@@ -118,6 +112,7 @@ defmodule Code.Normalizer do
|
||||
end
|
||||
end)
|
||||
|
||||
parts = maybe_add_trailing_newline(call_meta, parts, state)
|
||||
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
|
||||
else
|
||||
normalize_call(quoted, state)
|
||||
@@ -405,7 +400,8 @@ defmodule Code.Normalizer do
|
||||
defp allow_keyword?(:{}, _), do: false
|
||||
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
||||
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
|
||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
parts =
|
||||
@@ -427,6 +423,17 @@ defmodule Code.Normalizer do
|
||||
{:<<>>, meta, parts}
|
||||
end
|
||||
|
||||
defp maybe_add_trailing_newline(meta, parts, state) do
|
||||
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
|
||||
last = List.last(parts),
|
||||
true <- is_binary(last) and not String.ends_with?(last, "\n") do
|
||||
[_last | rest] = Enum.reverse(parts)
|
||||
Enum.reverse([last <> "\n" | rest])
|
||||
else
|
||||
_ -> parts
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
|
||||
Enum.map(parts, fn
|
||||
{:"::", interpolation_meta,
|
||||
|
||||
@@ -69,6 +69,22 @@ defprotocol Collectable do
|
||||
iex> Enum.into([1, 2, 3], MapSet.new())
|
||||
MapSet.new([1, 2, 3])
|
||||
|
||||
## Halting
|
||||
|
||||
The `:halt` flag will be given whenever the collection won't
|
||||
terminate correctly and must be used to clean up existing resources
|
||||
(such as sockets, file handles, etc).
|
||||
|
||||
Note it is not guaranteed that the accumulator given to halt will
|
||||
be the latest version of the accumulator returned by a previous call
|
||||
with `{:cont, elem}`. Therefore, you must track the collected results
|
||||
within the resource you intend to halt.
|
||||
|
||||
This is by design: ensuring halt is always called with the latest
|
||||
accumulator would make pure collectables (the ones that do not implement
|
||||
halt) expensive. However, given the collectables that must implement halt
|
||||
already need to track state, the burden of tracking the accumulator
|
||||
across invocations is put on them.
|
||||
"""
|
||||
|
||||
@type command :: {:cont, term} | :done | :halt
|
||||
|
||||
@@ -377,21 +377,27 @@ defmodule Config do
|
||||
end
|
||||
end
|
||||
|
||||
defp validate!(config, file) do
|
||||
Enum.all?(config, fn
|
||||
defp validate!(config, file) when is_list(config) do
|
||||
Enum.each(config, fn
|
||||
{app, value} when is_atom(app) ->
|
||||
if Keyword.keyword?(value) do
|
||||
true
|
||||
else
|
||||
if not Keyword.keyword?(value) do
|
||||
raise ArgumentError,
|
||||
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
|
||||
"to return keyword list, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
||||
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
|
||||
end)
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
defp validate!(config, file) do
|
||||
raise ArgumentError,
|
||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
||||
"of {atom, keyword} pairs, got: #{inspect(config)}"
|
||||
end
|
||||
end
|
||||
|
||||
+11
-16
@@ -829,7 +829,7 @@ defmodule Enum do
|
||||
"""
|
||||
@spec dedup(t) :: list
|
||||
def dedup(enumerable) when is_list(enumerable) do
|
||||
dedup_list(enumerable, []) |> :lists.reverse()
|
||||
dedup_list(enumerable)
|
||||
end
|
||||
|
||||
def dedup(enumerable) do
|
||||
@@ -2023,11 +2023,16 @@ defmodule Enum do
|
||||
operators work by using this function.
|
||||
"""
|
||||
@spec member?(t, element) :: boolean
|
||||
def member?(enumerable, element) when is_list(enumerable) do
|
||||
def member?(enumerable, element) do
|
||||
__in__(element, enumerable)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __in__(element, enumerable) when is_list(enumerable) do
|
||||
:lists.member(element, enumerable)
|
||||
end
|
||||
|
||||
def member?(enumerable, element) do
|
||||
def __in__(element, enumerable) do
|
||||
case Enumerable.member?(enumerable, element) do
|
||||
{:ok, element} when is_boolean(element) ->
|
||||
element
|
||||
@@ -4507,19 +4512,9 @@ defmodule Enum do
|
||||
|
||||
# dedup
|
||||
|
||||
defp dedup_list([value | tail], acc) do
|
||||
acc =
|
||||
case acc do
|
||||
[^value | _] -> acc
|
||||
_ -> [value | acc]
|
||||
end
|
||||
|
||||
dedup_list(tail, acc)
|
||||
end
|
||||
|
||||
defp dedup_list([], acc) do
|
||||
acc
|
||||
end
|
||||
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
|
||||
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
|
||||
defp dedup_list([]), do: []
|
||||
|
||||
## drop
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ defmodule Exception do
|
||||
@typedoc "The exception type"
|
||||
@type t :: %{
|
||||
required(:__struct__) => module,
|
||||
required(:__exception__) => true,
|
||||
required(:__exception__) => term,
|
||||
optional(atom) => any
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule Exception do
|
||||
@doc false
|
||||
@deprecated "Use Kernel.is_exception/1 instead"
|
||||
def exception?(term)
|
||||
def exception?(%_{__exception__: true}), do: true
|
||||
def exception?(%_{__exception__: _}), do: true
|
||||
def exception?(_), do: false
|
||||
|
||||
@doc """
|
||||
@@ -89,7 +89,7 @@ defmodule Exception do
|
||||
return a descriptive error message instead.
|
||||
"""
|
||||
@spec message(t) :: String.t()
|
||||
def message(%module{__exception__: true} = exception) do
|
||||
def message(%module{__exception__: _} = exception) do
|
||||
try do
|
||||
module.message(exception)
|
||||
rescue
|
||||
@@ -123,7 +123,7 @@ defmodule Exception do
|
||||
@spec normalize(:error, any, stacktrace) :: t
|
||||
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
|
||||
def normalize(kind, payload, stacktrace \\ [])
|
||||
def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload
|
||||
def normalize(:error, %_{__exception__: _} = payload, _stacktrace), do: payload
|
||||
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
|
||||
def normalize(_kind, payload, _stacktrace), do: payload
|
||||
|
||||
|
||||
+48
-22
@@ -1151,6 +1151,11 @@ defmodule File do
|
||||
dereferenced and have their contents copied instead when set to `true`. If the dereferenced
|
||||
files do not exist, than the operation fails. The default is `false`.
|
||||
|
||||
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
|
||||
source directories are copied to the destination directories after their contents are
|
||||
written. This is useful when source directories are read-only or have restricted
|
||||
permissions that must be preserved. The default is `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
# Copies file "a.txt" to "b.txt"
|
||||
@@ -1176,7 +1181,8 @@ defmodule File do
|
||||
"""
|
||||
@spec cp_r(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean()
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
) ::
|
||||
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
|
||||
|
||||
@@ -1198,6 +1204,7 @@ defmodule File do
|
||||
def cp_r(source, destination, options) when is_list(options) do
|
||||
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
|
||||
dereference? = Keyword.get(options, :dereference_symlinks, false)
|
||||
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
|
||||
|
||||
source =
|
||||
source
|
||||
@@ -1217,7 +1224,14 @@ defmodule File do
|
||||
else
|
||||
dereference = if dereference?, do: MapSet.new(), else: nil
|
||||
|
||||
case do_cp_r(source, destination, on_conflict, dereference, []) do
|
||||
case do_cp_r(
|
||||
source,
|
||||
destination,
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_directory_permissions?,
|
||||
[]
|
||||
) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
end
|
||||
@@ -1241,7 +1255,8 @@ defmodule File do
|
||||
"""
|
||||
@spec cp_r!(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean()
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
) :: [binary]
|
||||
def cp_r!(source, destination, options \\ []) do
|
||||
case cp_r(source, destination, options) do
|
||||
@@ -1258,7 +1273,7 @@ defmodule File do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cp_r(src, dest, on_conflict, dereference, acc) when is_list(acc) do
|
||||
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
|
||||
case :elixir_utils.read_link_type(src) do
|
||||
{:ok, :regular} ->
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
@@ -1278,7 +1293,7 @@ defmodule File do
|
||||
{:error, :eloop, src}
|
||||
else
|
||||
dereference = MapSet.put(dereference, resolved)
|
||||
do_cp_r(resolved, dest, on_conflict, dereference, acc)
|
||||
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
|
||||
end
|
||||
|
||||
{:ok, link} ->
|
||||
@@ -1293,23 +1308,34 @@ defmodule File do
|
||||
{:ok, files} ->
|
||||
case mkdir(dest) do
|
||||
success when success in [:ok, {:error, :eexist}] ->
|
||||
case copy_file_mode(src, dest) do
|
||||
:ok ->
|
||||
Enum.reduce_while(files, [dest | acc], fn x, acc ->
|
||||
case do_cp_r(
|
||||
Path.join(src, x),
|
||||
Path.join(dest, x),
|
||||
on_conflict,
|
||||
dereference,
|
||||
acc
|
||||
) do
|
||||
{:error, _, _} = error -> {:halt, error}
|
||||
acc -> {:cont, acc}
|
||||
end
|
||||
end)
|
||||
files
|
||||
|> Enum.reduce_while([dest | acc], fn x, acc ->
|
||||
case do_cp_r(
|
||||
Path.join(src, x),
|
||||
Path.join(dest, x),
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_dir_perms?,
|
||||
acc
|
||||
) do
|
||||
{:error, _, _} = error -> {:halt, error}
|
||||
acc -> {:cont, acc}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, src}
|
||||
files when preserve_dir_perms? ->
|
||||
# Change the directory after writing files in case
|
||||
# it was originally read only
|
||||
case copy_file_mode(src, dest) do
|
||||
:ok -> files
|
||||
{:error, reason} -> {:error, reason, src}
|
||||
end
|
||||
|
||||
files ->
|
||||
files
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -1329,7 +1355,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
# If we reach this clause, there was an error while processing a file.
|
||||
defp do_cp_r(_, _, _, _, acc) do
|
||||
defp do_cp_r(_, _, _, _, _, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
|
||||
+133
-101
@@ -344,15 +344,12 @@ defmodule Float do
|
||||
|
||||
"""
|
||||
@spec round(float, precision_range) :: float
|
||||
# This implementation is slow since it relies on big integers.
|
||||
# Faster implementations are available on more recent papers
|
||||
# and could be implemented in the future.
|
||||
def round(float, precision \\ 0)
|
||||
|
||||
def round(float, 0) when float == 0.0, do: float
|
||||
|
||||
def round(float, 0) when is_float(float) do
|
||||
case float |> :erlang.round() |> :erlang.float() do
|
||||
case :erlang.round(float) * 1.0 do
|
||||
zero when zero == 0.0 and float < 0.0 -> -0.0
|
||||
rounded -> rounded
|
||||
end
|
||||
@@ -366,135 +363,170 @@ defmodule Float do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
# Decimal-place rounding via exact rational scaling. This is the bignum
|
||||
# core used by reference implementations like David M. Gay's "Correctly
|
||||
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
|
||||
# @doc above), Python's round(), and Java's BigDecimal.setScale.
|
||||
#
|
||||
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
|
||||
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
|
||||
# Because precision is bounded to 0..15, the product fits in ~103 bits
|
||||
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
|
||||
# directly without approximation.
|
||||
# 3. Round the exact rational to an integer per the requested mode
|
||||
# (half_up / floor / ceil) using quotient and remainder.
|
||||
# 4. Emit the float closest to rounded_int / 10^precision:
|
||||
# - fast path: when rounded_int < 2^53, both operands are exactly
|
||||
# representable as floats and IEEE division is correctly rounded.
|
||||
# - slow path: bignum alignment + manual mantissa extraction with
|
||||
# round-to-nearest-even for the trailing bit.
|
||||
#
|
||||
# The integer-rounding decision (step 3) and the binary-emission decision
|
||||
# (step 4) are deliberately independent: step 3 picks the exact rational
|
||||
# the user asked for, step 4 picks the closest float to that rational.
|
||||
# Conflating them is the classic source of double-rounding bugs.
|
||||
#
|
||||
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
|
||||
# for round-trip printing) but target different problems or assume
|
||||
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
|
||||
# At precision <= 15, the exact path is small, easy to audit, and fast
|
||||
# enough that a more complex algorithm has not been justified by benchmarks.
|
||||
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
|
||||
|
||||
defp round(float, precision, rounding) do
|
||||
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||
{num, count} = decompose(significant, 1)
|
||||
count = count - exp + 1023
|
||||
defp round(float, precision, mode) do
|
||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||
|
||||
cond do
|
||||
# Precision beyond 15 digits
|
||||
count >= 104 ->
|
||||
case rounding do
|
||||
:ceil when sign === 0 -> 1 / power_of_10(precision)
|
||||
:floor when sign === 1 -> -1 / power_of_10(precision)
|
||||
:ceil when sign === 1 -> -0.0
|
||||
:half_up when sign === 1 -> -0.0
|
||||
_ -> 0.0
|
||||
end
|
||||
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump
|
||||
# to 10^-precision; everything else rounds to signed zero).
|
||||
exp == 0 ->
|
||||
tiny_round(sign, precision, mode)
|
||||
|
||||
# We are asking more precision than we have
|
||||
count <= precision ->
|
||||
# |float| >= 2^52 — has no fractional bits, return unchanged.
|
||||
exp - 1075 >= 0 ->
|
||||
float
|
||||
|
||||
true ->
|
||||
# Difference in precision between float and asked precision
|
||||
# We subtract 1 because we need to calculate the remainder too
|
||||
diff = count - precision - 1
|
||||
|
||||
# Get up to latest so we calculate the remainder
|
||||
power_of_10 = power_of_10(diff)
|
||||
|
||||
# Convert the numerand to decimal base
|
||||
num = num * power_of_5(count)
|
||||
|
||||
# Move to the given precision - 1
|
||||
num = div(num, power_of_10)
|
||||
div = div(num, 10)
|
||||
num = rounding(rounding, sign, num, div)
|
||||
|
||||
# Convert back to float without loss
|
||||
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
|
||||
den = power_of_10(precision)
|
||||
boundary = den <<< 52
|
||||
|
||||
cond do
|
||||
num == 0 and sign == 1 ->
|
||||
-0.0
|
||||
|
||||
num == 0 ->
|
||||
0.0
|
||||
|
||||
num >= boundary ->
|
||||
{den, exp} = scale_down(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
|
||||
true ->
|
||||
{num, exp} = scale_up(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
end
|
||||
mantissa = @power_of_2_to_52 ||| mantissa
|
||||
shift = 1075 - exp
|
||||
do_round(sign, mantissa, shift, precision, mode)
|
||||
end
|
||||
end
|
||||
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, initial)
|
||||
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign.
|
||||
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do
|
||||
tiny_round(sign, precision, mode)
|
||||
end
|
||||
|
||||
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
|
||||
end
|
||||
defp do_round(sign, mantissa, shift, precision, mode) do
|
||||
power = power_of_10(precision)
|
||||
product = mantissa * power
|
||||
half = 1 <<< (shift - 1)
|
||||
quotient = product >>> shift
|
||||
remainder = product - (quotient <<< shift)
|
||||
rounded_int = round_step(mode, sign, quotient, remainder, half)
|
||||
|
||||
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, last_count, acc)
|
||||
end
|
||||
cond do
|
||||
rounded_int == 0 ->
|
||||
signed_zero(sign)
|
||||
|
||||
defp decompose(<<>>, _count, last_count, acc) do
|
||||
{acc, last_count}
|
||||
end
|
||||
rounded_int < @power_of_2_to_52 <<< 1 ->
|
||||
# Both rounded_int and power fit in 53 bits, so IEEE float division
|
||||
# is correctly rounded.
|
||||
result = rounded_int / power
|
||||
if sign == 1, do: -result, else: result
|
||||
|
||||
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
|
||||
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
|
||||
|
||||
defp scale_down(num, den, exp) do
|
||||
new_den = den <<< 1
|
||||
|
||||
if num < new_den do
|
||||
{den >>> 52, exp}
|
||||
else
|
||||
scale_down(num, new_den, exp + 1)
|
||||
true ->
|
||||
bignum_to_float(sign, rounded_int, power)
|
||||
end
|
||||
end
|
||||
|
||||
defp decimal_to_float(sign, num, den, exp) do
|
||||
quo = div(num, den)
|
||||
rem = num - quo * den
|
||||
defp round_step(:half_up, _sign, quotient, remainder, half) do
|
||||
if remainder >= half, do: quotient + 1, else: quotient
|
||||
end
|
||||
|
||||
tmp =
|
||||
case den >>> 1 do
|
||||
den when rem > den -> quo + 1
|
||||
den when rem < den -> quo
|
||||
_ when (quo &&& 1) === 1 -> quo + 1
|
||||
_ -> quo
|
||||
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient
|
||||
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1
|
||||
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient
|
||||
|
||||
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1
|
||||
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient
|
||||
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
|
||||
|
||||
defp signed_zero(0), do: 0.0
|
||||
defp signed_zero(1), do: -0.0
|
||||
|
||||
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
|
||||
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
|
||||
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
|
||||
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
|
||||
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
|
||||
|
||||
# Slow path: emit float closest to `sign * rounded_int / power` when
|
||||
# rounded_int >= 2^53. The binary emission step is always IEEE
|
||||
# round-to-nearest-even, regardless of the integer-rounding mode.
|
||||
defp bignum_to_float(sign, rounded_int, power) do
|
||||
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
|
||||
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
|
||||
|
||||
quotient = div(numerator, denominator)
|
||||
remainder = numerator - quotient * denominator
|
||||
half = denominator >>> 1
|
||||
|
||||
mantissa =
|
||||
cond do
|
||||
remainder > half -> quotient + 1
|
||||
remainder < half -> quotient
|
||||
(quotient &&& 1) === 1 -> quotient + 1
|
||||
true -> quotient
|
||||
end
|
||||
|
||||
tmp = tmp - @power_of_2_to_52
|
||||
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
|
||||
tmp
|
||||
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper
|
||||
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient
|
||||
# or when rounding carries. Rebalance into the canonical [2^52, 2^53)
|
||||
# range so the 52-bit packing below doesn't silently truncate.
|
||||
{mantissa, exp} =
|
||||
if mantissa == @power_of_2_to_52 <<< 1,
|
||||
do: {@power_of_2_to_52, exp + 1},
|
||||
else: {mantissa, exp}
|
||||
|
||||
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
|
||||
result
|
||||
end
|
||||
|
||||
defp rounding(:floor, 1, _num, div), do: div + 1
|
||||
defp rounding(:ceil, 0, _num, div), do: div + 1
|
||||
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53)
|
||||
# and the resulting float = numerator/denominator * 2^(exp-52).
|
||||
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
|
||||
new_power = power <<< shift_adjust
|
||||
|
||||
defp rounding(:half_up, _sign, num, div) do
|
||||
case rem(num, 10) do
|
||||
rem when rem < 5 -> div
|
||||
rem when rem >= 5 -> div + 1
|
||||
if rounded_int < new_power <<< 53,
|
||||
do: {rounded_int, new_power, 52 + shift_adjust},
|
||||
else: {rounded_int, new_power <<< 1, 53 + shift_adjust}
|
||||
end
|
||||
|
||||
defp align(rounded_int, power, shift_adjust) do
|
||||
shifted = rounded_int <<< -shift_adjust
|
||||
|
||||
cond do
|
||||
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
|
||||
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
|
||||
true -> {shifted <<< 1, power, 51 + shift_adjust}
|
||||
end
|
||||
end
|
||||
|
||||
defp rounding(_, _, _, div), do: div
|
||||
defp bit_length(0), do: 0
|
||||
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
|
||||
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
|
||||
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
|
||||
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
|
||||
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
|
||||
defp bit_length(_integer, acc), do: acc
|
||||
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_10(unquote(x)), do: unquote(acc)
|
||||
Enum.reduce(0..15, 1, fn exponent, acc ->
|
||||
defp power_of_10(unquote(exponent)), do: unquote(acc)
|
||||
acc * 10
|
||||
end)
|
||||
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_5(unquote(x)), do: unquote(acc)
|
||||
acc * 5
|
||||
end)
|
||||
|
||||
@doc """
|
||||
Returns a pair of integers whose ratio is exactly equal
|
||||
to the original float and with a positive denominator.
|
||||
|
||||
@@ -567,9 +567,12 @@ defmodule GenServer do
|
||||
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
|
||||
after a delay to attempt a restart.
|
||||
|
||||
Returning `{:stop, reason}` will cause `start_link/3` to return
|
||||
`{:error, reason}` and the process to exit with reason `reason` without
|
||||
entering the loop or calling `c:terminate/2`.
|
||||
Returning `{:error, reason}` will cause `start_link/3` to return
|
||||
`{:error, reason}`.
|
||||
|
||||
Returning `{:stop, reason}` will the process to exit with reason `reason`,
|
||||
without entering the loop or calling `c:terminate/2`. `start_link/3` will
|
||||
return `{:error, reason}`, but only if the caller is trapping exits.
|
||||
"""
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok, state}
|
||||
|
||||
@@ -669,6 +669,11 @@ defimpl Inspect, for: Any do
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
end
|
||||
|
||||
# A temporary clause to deal with native records until they are officially supported
|
||||
def inspect(native_record, _opts) do
|
||||
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
def inspect_as_struct(map, name, infos, opts) do
|
||||
open = color_doc("#" <> name <> "<", :map, opts)
|
||||
sep = color_doc(",", :map, opts)
|
||||
|
||||
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items, with the exception of
|
||||
printable strings and printable charlists which use the `:printable_limit` option.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `100` since
|
||||
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `200` since
|
||||
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
|
||||
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
|
||||
charlists: :infer,
|
||||
custom_options: [],
|
||||
inspect_fun: &Inspect.inspect/2,
|
||||
limit: 100,
|
||||
limit: 200,
|
||||
pretty: false,
|
||||
printable_limit: 4096,
|
||||
safe: true,
|
||||
|
||||
+19
-13
@@ -2752,7 +2752,7 @@ defmodule Kernel do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{__exception__: true} -> true
|
||||
%_{__exception__: _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2764,8 +2764,7 @@ defmodule Kernel do
|
||||
quote do
|
||||
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
:erlang.is_map_key(:__exception__, unquote(term))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2792,7 +2791,7 @@ defmodule Kernel do
|
||||
case unquote(name) do
|
||||
name when is_atom(name) ->
|
||||
case unquote(term) do
|
||||
%{__struct__: ^name, __exception__: true} -> true
|
||||
%{__struct__: ^name, __exception__: _} -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
@@ -2810,8 +2809,7 @@ defmodule Kernel do
|
||||
(is_atom(unquote(name)) or :fail) and
|
||||
:erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
:erlang.is_map_key(:__exception__, unquote(term))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4725,7 +4723,7 @@ defmodule Kernel do
|
||||
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step)))
|
||||
|
||||
_ when in_body? ->
|
||||
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
|
||||
quote(do: Elixir.Enum.__in__(unquote(left), unquote(right)))
|
||||
|
||||
_ ->
|
||||
raise_on_invalid_args_in_2(right)
|
||||
@@ -6423,12 +6421,20 @@ defmodule Kernel do
|
||||
{microsecond, _precision} = duration.microsecond
|
||||
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
|
||||
|
||||
duration.week * unquote(week_in_ms) +
|
||||
duration.day * unquote(day_in_ms) +
|
||||
duration.hour * unquote(hour_in_ms) +
|
||||
duration.minute * 60_000 +
|
||||
duration.second * 1000 +
|
||||
millisecond
|
||||
total =
|
||||
duration.week * unquote(week_in_ms) +
|
||||
duration.day * unquote(day_in_ms) +
|
||||
duration.hour * unquote(hour_in_ms) +
|
||||
duration.minute * 60_000 +
|
||||
duration.second * 1000 +
|
||||
millisecond
|
||||
|
||||
if total < 0 do
|
||||
raise ArgumentError,
|
||||
"duration must be positive, got: #{inspect(duration)}"
|
||||
end
|
||||
|
||||
total
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,9 +25,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
|
||||
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
|
||||
{:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
|
||||
@@ -610,8 +610,7 @@ defmodule Kernel.Typespec do
|
||||
types =
|
||||
:lists.map(
|
||||
fn %{field: field} ->
|
||||
default_type = if field == :__exception__, do: true, else: quote(do: term())
|
||||
{field, Keyword.get(fields, field, default_type)}
|
||||
{field, Keyword.get(fields, field, quote(do: term()))}
|
||||
end,
|
||||
struct_info
|
||||
)
|
||||
|
||||
@@ -266,7 +266,7 @@ defmodule Kernel.Utils do
|
||||
module.exception([])
|
||||
end
|
||||
|
||||
def raise(%_{__exception__: true} = exception) do
|
||||
def raise(%_{__exception__: _} = exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
|
||||
+10
-10
@@ -270,34 +270,34 @@ defmodule Keyword do
|
||||
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
|
||||
{:ok, keyword()} | {:error, [atom]}
|
||||
def validate(keyword, values) when is_list(keyword) and is_list(values) do
|
||||
validate(keyword, values, [], [], [])
|
||||
validate(keyword, values, [], keyword, [])
|
||||
end
|
||||
|
||||
defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
|
||||
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
|
||||
case find_key!(key, values1, values2) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
|
||||
:error ->
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, acc, [key | bad_keys])
|
||||
validate(keyword, values1, values2, original, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], values1, values2, acc, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
|
||||
defp validate([], values1, values2, original, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
|
||||
end
|
||||
|
||||
defp validate([], _values1, _values2, _acc, bad_keys) do
|
||||
defp validate([], _values1, _values2, _original, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
end
|
||||
|
||||
defp validate([pair | _], _values1, _values2, _acc, []) do
|
||||
defp validate([pair | _], _values1, _values2, _original, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
@@ -965,7 +965,7 @@ defmodule Keyword do
|
||||
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
|
||||
true
|
||||
|
||||
Comparison between values is done with `===/3`,
|
||||
Comparison between values is done with `===/2`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Keyword.equal?([a: 1.0], [a: 1])
|
||||
|
||||
@@ -973,7 +973,7 @@ defmodule Macro do
|
||||
def struct_info!(module, env) when is_atom(module) do
|
||||
meta = [line: env.line]
|
||||
|
||||
case :elixir_map.maybe_load_struct_info(meta, module, env) do
|
||||
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
|
||||
{:ok, info} ->
|
||||
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
|
||||
info
|
||||
@@ -2196,7 +2196,7 @@ defmodule Macro do
|
||||
Please check `expand_literals/2` for use cases and pitfalls.
|
||||
"""
|
||||
@doc since: "1.14.1"
|
||||
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
|
||||
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term()
|
||||
def expand_literals(ast, acc, fun)
|
||||
|
||||
def expand_literals({:__aliases__, meta, args}, acc, fun) do
|
||||
|
||||
@@ -367,7 +367,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
|
||||
{:ok, t} | {:error, String.t()}
|
||||
def define_require(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
|
||||
@@ -1109,7 +1109,7 @@ defmodule Map do
|
||||
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
|
||||
false
|
||||
|
||||
Comparison between keys and values is done with `===/3`,
|
||||
Comparison between keys and values is done with `===/2`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Map.equal?(%{a: 1.0}, %{a: 1})
|
||||
|
||||
+177
-31
@@ -4,7 +4,7 @@
|
||||
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
alias Module.Types.{Descr, Expr, Pattern, Helpers}
|
||||
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
|
||||
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
@@ -251,17 +251,27 @@ defmodule Module.Types do
|
||||
context ->
|
||||
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
clauses_indexes =
|
||||
for type_index <- pending,
|
||||
not skip_unused_clause?(info, type_index),
|
||||
{clause_index, ^type_index} <- mapping,
|
||||
do: clause_index
|
||||
if pending != [] do
|
||||
{used_indexes, unused_indexes} =
|
||||
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
|
||||
{used_indexes, unused_indexes} ->
|
||||
if type_index in pending and not skip_unused_clause?(info, type_index) do
|
||||
{used_indexes, [clause_index | unused_indexes]}
|
||||
else
|
||||
{[clause_index | used_indexes], unused_indexes}
|
||||
end
|
||||
end)
|
||||
|
||||
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
|
||||
|
||||
Enum.reduce(unused_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -321,28 +331,106 @@ defmodule Module.Types do
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
base_info = {:def, kind, fun, expected}
|
||||
|
||||
{_, _, _, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], context}, fn
|
||||
{meta, args, guards, body}, {index, total, previous, mapping, inferred, acc_context} ->
|
||||
case clauses do
|
||||
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
|
||||
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context)
|
||||
|
||||
_ ->
|
||||
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
|
||||
guards = []
|
||||
previous = Pattern.init_previous()
|
||||
fresh_context = fresh_context(context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, _, _, _, head_context} =
|
||||
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
|
||||
|
||||
# Compute the intersected arrows from the function call
|
||||
{:super, meta, call_args} = body
|
||||
{_kind, call_fun} = Keyword.fetch!(meta, :super)
|
||||
term = Descr.term()
|
||||
of_fun = &Expr.of_expr/5
|
||||
|
||||
{arrows, body_context} =
|
||||
Apply.local_arrows(call_fun, call_args, term, body, stack, head_context, of_fun)
|
||||
|
||||
# For each arrow, compute the default arrow
|
||||
{_, _, mapping, inferred} =
|
||||
Enum.reduce(arrows, {0, 0, [], []}, fn
|
||||
{clause_domain, return_type}, {index, total, mapping, inferred} ->
|
||||
of_fun = &Expr.of_expr(&1, &2, body, stack, &3)
|
||||
|
||||
{_clause_args, clause_context} =
|
||||
Helpers.zip_map_reduce(call_args, clause_domain, head_context, of_fun)
|
||||
|
||||
clause_types = Pattern.of_domain(trees, stack, clause_context)
|
||||
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, clause_types, return_type, total - 1, [])
|
||||
|
||||
total = if type_index == -1, do: total + 1, else: total
|
||||
{index + 1, total, [{0, index} | mapping], inferred}
|
||||
end)
|
||||
|
||||
domain =
|
||||
case inferred do
|
||||
[_] ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
inferred
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
end
|
||||
|
||||
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
end
|
||||
|
||||
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
|
||||
{_, _, _, domain, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
|
||||
{meta, args, guards, body},
|
||||
{index, total, previous, domain, mapping, inferred, acc_context} ->
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, previous, context} =
|
||||
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
|
||||
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
|
||||
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, Descr.term(), body, stack, context)
|
||||
Expr.of_expr(body, Descr.term(), body, stack, head_context)
|
||||
|
||||
args_types = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, args_types, return_type, total - 1, [])
|
||||
|
||||
domain =
|
||||
case domain do
|
||||
[] ->
|
||||
args_types
|
||||
|
||||
_ ->
|
||||
head_args_types = Pattern.of_domain(trees, stack, head_context)
|
||||
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
|
||||
end
|
||||
|
||||
if type_index == -1 do
|
||||
{index + 1, total + 1, previous, [{index, total} | mapping], inferred, context}
|
||||
mapping = [{index, total} | mapping]
|
||||
{index + 1, total + 1, previous, domain, mapping, inferred, context}
|
||||
else
|
||||
{index + 1, total, previous, [{index, type_index} | mapping], inferred, context}
|
||||
mapping = [{index, type_index} | mapping]
|
||||
{index + 1, total, previous, domain, mapping, inferred, context}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@@ -352,19 +440,69 @@ defmodule Module.Types do
|
||||
|
||||
domain =
|
||||
case clauses_types do
|
||||
[_] ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
clauses_types
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
[_] -> nil
|
||||
_ -> domain
|
||||
end
|
||||
|
||||
inferred = {:infer, domain, Enum.reverse(clauses_types)}
|
||||
{inferred, mapping, restore_context(clauses_context, context)}
|
||||
end
|
||||
|
||||
defp compute_domain(
|
||||
[arg | args_types],
|
||||
[head_arg | head_args_types],
|
||||
[no_prev_arg | no_prev_args_types],
|
||||
[d | domain]
|
||||
) do
|
||||
[
|
||||
# This is an optimization that broadens the domain, but it is acceptable
|
||||
# because the domain is used for reverse arrows and not type checking.
|
||||
#
|
||||
# The overall idea is that, if we have a function with three clauses,
|
||||
# the domain is computed by unioning their inferred types. However, their
|
||||
# inferred types often have the different of the previous clauses:
|
||||
#
|
||||
# union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
|
||||
#
|
||||
# Where `rN` represents the refinement in every function body.
|
||||
#
|
||||
# What this function does is, if the type of a given arg in a clause
|
||||
# before and after the body is the same (meaning r3 is term), then
|
||||
# we replace all of `(c3 - c2 - c1)` by just `c3`, which removes
|
||||
# many of the differences in the node. However, keep in mind that,
|
||||
# because `r2` may have refine `c2` in the previous clause, the domain
|
||||
# may end-up being broader. Take this example:
|
||||
#
|
||||
# % %{..., foo: integer()} -> binary()
|
||||
# def example(%{foo: var}), do: Integer.to_string(var)
|
||||
#
|
||||
# % %{...} and not %{..., foo: term()} -> :error
|
||||
# def example(%{}), do: :error
|
||||
#
|
||||
# The actual domain is:
|
||||
#
|
||||
# %{..., foo: not_set()} or %{..., foo: integer()}
|
||||
# #=> %{..., foo: if_set(integer())}
|
||||
#
|
||||
# But we will infer:
|
||||
#
|
||||
# %{...} or %{..., foo: integer()}
|
||||
# #=> %{...}
|
||||
#
|
||||
# We lose precision but this is exactly what we want: to have simpler types.
|
||||
# Furthermore, the signature used in type checking is not refined in any way,
|
||||
# so type checking is still sound.
|
||||
if arg == head_arg do
|
||||
Descr.union(Descr.upper_bound(no_prev_arg), d)
|
||||
else
|
||||
Descr.union(arg, d)
|
||||
end
|
||||
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
|
||||
]
|
||||
end
|
||||
|
||||
defp compute_domain([], [], [], []), do: []
|
||||
|
||||
# We check for term equality of types as an optimization
|
||||
# to reduce the amount of check we do at runtime.
|
||||
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
|
||||
@@ -437,7 +575,9 @@ defmodule Module.Types do
|
||||
# The mode to be used, see the @modes attribute
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :use)
|
||||
reverse_arrow: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -459,20 +599,26 @@ defmodule Module.Types do
|
||||
# Local signatures used by local handler
|
||||
local_sigs: %{},
|
||||
# Track which clauses have been used across private local calls
|
||||
local_used: %{}
|
||||
local_used: %{},
|
||||
# Cached reverse arrows
|
||||
reverse_arrows: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp fresh_stack(stack, mode, function) when mode in @modes do
|
||||
%{stack | mode: mode, function: function}
|
||||
%{stack | mode: mode, function: function, reverse_arrow: nil}
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
%{context | vars: %{}, failed: false}
|
||||
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
|
||||
end
|
||||
|
||||
defp restore_context(later_context, %{vars: vars, failed: failed}) do
|
||||
%{later_context | vars: vars, failed: failed}
|
||||
defp restore_context(later_context, %{
|
||||
vars: vars,
|
||||
failed: failed,
|
||||
reverse_arrows: reverse_arrows
|
||||
}) do
|
||||
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
|
||||
@@ -393,6 +393,7 @@ defmodule Module.Types.Apply do
|
||||
case sized_order(name, left, right, expected) do
|
||||
{arg, expected, precise?, return} ->
|
||||
{actual, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
# expected is a static type, so we can use subtype? rather than compatible?
|
||||
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
|
||||
{result, context}
|
||||
|
||||
@@ -497,50 +498,55 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp do_remote(:lists, :member, [arg, list] = args, expected, expr, stack, context, of_fun)
|
||||
when is_list(list) and list != [] do
|
||||
case booleaness(expected) do
|
||||
{polarity, _maybe_or_always} ->
|
||||
{return, acc} =
|
||||
case polarity do
|
||||
true -> {@atom_true, none()}
|
||||
false -> {@atom_false, term()}
|
||||
when is_list(list) do
|
||||
if list == [] or Enum.any?(list, &match?({:|, _, [_, _]}, &1)) do
|
||||
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
|
||||
else
|
||||
case booleaness(expected) do
|
||||
{polarity, _maybe_or_always} ->
|
||||
{return, acc} =
|
||||
case polarity do
|
||||
true -> {@atom_true, none()}
|
||||
false -> {@atom_false, term()}
|
||||
end
|
||||
|
||||
{expected, singleton?, context} =
|
||||
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
|
||||
{type, context} = of_fun.(literal, term(), expr, stack, context)
|
||||
|
||||
if singleton?(type) do
|
||||
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
|
||||
{acc, all_singleton?, context}
|
||||
else
|
||||
acc = if polarity, do: union(acc, type), else: acc
|
||||
{acc, false, context}
|
||||
end
|
||||
end)
|
||||
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
|
||||
cond do
|
||||
# expected can have dynamic terms but, since we have verified
|
||||
# them to be singletons, we can compute the upper bound.
|
||||
singleton? and subtype?(arg_type, upper_bound(expected)) ->
|
||||
{return, context}
|
||||
|
||||
# Singleton types with reverse polarity are negated, so we don't check for disjoint
|
||||
(singleton? and not polarity) or not is_warning(stack) ->
|
||||
{return(boolean(), [arg_type, expected], stack), context}
|
||||
|
||||
# Nothing in common between left and right, emit a warning
|
||||
disjoint?(arg_type, expected) ->
|
||||
error = {:mismatched_comparison, arg_type, list(expected)}
|
||||
remote_error(error, :lists, :member, 2, expr, stack, context)
|
||||
|
||||
true ->
|
||||
{return(boolean(), [arg_type, expected], stack), context}
|
||||
end
|
||||
|
||||
{expected, singleton?, context} =
|
||||
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
|
||||
{type, context} = of_fun.(literal, term(), expr, stack, context)
|
||||
|
||||
if singleton?(type) do
|
||||
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
|
||||
{acc, all_singleton?, context}
|
||||
else
|
||||
acc = if polarity, do: union(acc, type), else: acc
|
||||
{acc, false, context}
|
||||
end
|
||||
end)
|
||||
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
|
||||
cond do
|
||||
# Return a precise result
|
||||
singleton? and subtype?(arg_type, expected) ->
|
||||
{return(return, [arg_type, expected], stack), context}
|
||||
|
||||
# Singleton types with reverse polarity are negated, so we don't check for disjoint
|
||||
(singleton? and not polarity) or not is_warning(stack) ->
|
||||
{return(boolean(), [arg_type, expected], stack), context}
|
||||
|
||||
# Nothing in common between left and right, emit a warning
|
||||
disjoint?(arg_type, expected) ->
|
||||
error = {:mismatched_comparison, arg_type, list(expected)}
|
||||
remote_error(error, :lists, :member, 2, expr, stack, context)
|
||||
|
||||
true ->
|
||||
{return(boolean(), [arg_type, expected], stack), context}
|
||||
end
|
||||
|
||||
_ ->
|
||||
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
|
||||
_ ->
|
||||
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -584,17 +590,29 @@ defmodule Module.Types.Apply do
|
||||
|
||||
{expected, precise?} =
|
||||
case fun do
|
||||
:length when :erlang.xor(polarity, literal > 0) ->
|
||||
{@empty_list, literal == 0}
|
||||
:length when polarity and literal == 0 ->
|
||||
{@empty_list, true}
|
||||
|
||||
:length when not polarity and literal == 0 ->
|
||||
{@non_empty_list, true}
|
||||
|
||||
:length when polarity ->
|
||||
{@non_empty_list, false}
|
||||
|
||||
:length ->
|
||||
{@non_empty_list, literal == 0}
|
||||
{@list, false}
|
||||
|
||||
:map_size when :erlang.xor(polarity, literal > 0) ->
|
||||
{@empty_map, literal == 0}
|
||||
:map_size when polarity and literal == 0 ->
|
||||
{@empty_map, true}
|
||||
|
||||
:map_size when not polarity and literal == 0 ->
|
||||
{@non_empty_map, true}
|
||||
|
||||
:map_size when polarity ->
|
||||
{@non_empty_map, false}
|
||||
|
||||
:map_size ->
|
||||
{@non_empty_map, literal == 0}
|
||||
{open_map(), false}
|
||||
|
||||
:tuple_size when polarity ->
|
||||
{tuple(List.duplicate(term(), literal)), true}
|
||||
@@ -604,6 +622,8 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
{actual, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
|
||||
# expected here is a static type, so subtyping relation is fine
|
||||
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
|
||||
|
||||
# We can skip return compare because literal is always an integer,
|
||||
@@ -631,7 +651,7 @@ defmodule Module.Types.Apply do
|
||||
if singleton?(type) do
|
||||
expected = if polarity, do: type, else: negation(type)
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
result = if subtype?(arg_type, expected), do: return, else: boolean()
|
||||
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
|
||||
|
||||
# Because reverse polarity means we will infer negated types
|
||||
# (which are naturally disjoint), we skip checks in such cases
|
||||
@@ -718,8 +738,8 @@ defmodule Module.Types.Apply do
|
||||
|
||||
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
|
||||
case booleaness(expected) do
|
||||
{true, _} -> sized_order(invert_order(name), fun, size, arg, @atom_true)
|
||||
{false, _} -> sized_order(name, fun, size, arg, @atom_false)
|
||||
{true, _} -> sized_order(mirror_order(name), fun, size, arg, @atom_true)
|
||||
{false, _} -> sized_order(invert_order(mirror_order(name)), fun, size, arg, @atom_false)
|
||||
_ -> :none
|
||||
end
|
||||
|
||||
@@ -771,6 +791,11 @@ defmodule Module.Types.Apply do
|
||||
defp invert_order(:>), do: :"=<"
|
||||
defp invert_order(:<), do: :>=
|
||||
|
||||
defp mirror_order(:>=), do: :"=<"
|
||||
defp mirror_order(:"=<"), do: :>=
|
||||
defp mirror_order(:>), do: :<
|
||||
defp mirror_order(:<), do: :>
|
||||
|
||||
@doc """
|
||||
Returns the domain of an unknown module.
|
||||
|
||||
@@ -1350,33 +1375,62 @@ defmodule Module.Types.Apply do
|
||||
## Local
|
||||
|
||||
def local(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
|
||||
{local_info, domain, context} = local_domain(fun, args, expected, meta, stack, context)
|
||||
{updated_used?, info, domain, context} =
|
||||
local_domain(fun, args, expected, meta, stack, context)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
|
||||
|
||||
local_apply(local_info, fun, args_types, expr, stack, context)
|
||||
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
|
||||
{_indexes, type, context} -> {type, context}
|
||||
{type, context} -> {type, context}
|
||||
end
|
||||
end
|
||||
|
||||
def local_arrows(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
|
||||
{updated_used?, info, domain, context} =
|
||||
local_domain(fun, args, expected, meta, stack, context)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
|
||||
|
||||
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
|
||||
{indexes, _type, context} -> {indexes_to_arrows(indexes, info), context}
|
||||
{type, context} -> {[{Enum.map(args, fn _ -> type end), type}], context}
|
||||
end
|
||||
end
|
||||
|
||||
defp indexes_to_arrows(indexes, {_, _domain, clauses}),
|
||||
do: indexes_to_arrows(Enum.reverse(indexes), clauses, 0)
|
||||
|
||||
defp indexes_to_arrows([index | indexes], [clause | clauses], index),
|
||||
do: [clause | indexes_to_arrows(indexes, clauses, index + 1)]
|
||||
|
||||
defp indexes_to_arrows([_ | _] = indexes, [_clause | clauses], index),
|
||||
do: indexes_to_arrows(indexes, clauses, index + 1)
|
||||
|
||||
defp indexes_to_arrows([], _clauses, _index),
|
||||
do: []
|
||||
|
||||
defp local_domain(fun, args, expected, meta, stack, context) do
|
||||
arity = length(args)
|
||||
|
||||
case stack.local_handler.(meta, {fun, arity}, stack, context) do
|
||||
false ->
|
||||
{{false, :none}, List.duplicate(term(), arity), context}
|
||||
{false, :none, List.duplicate(term(), arity), context}
|
||||
|
||||
{kind, info, context} ->
|
||||
update_used? = is_warning(stack) and kind == :defp
|
||||
|
||||
if info == :none do
|
||||
{{update_used?, :none}, List.duplicate(term(), arity), context}
|
||||
{update_used?, :none, List.duplicate(term(), arity), context}
|
||||
else
|
||||
{{update_used?, info}, filter_domain(info, expected, arity), context}
|
||||
{update_used?, info, filter_domain(info, expected, arity), context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp local_apply({update_used?, :none}, fun, args_types, _expr, _stack, context) do
|
||||
defp local_apply(update_used?, :none, fun, args_types, _expr, _stack, context) do
|
||||
if update_used? do
|
||||
{dynamic(), put_in(context.local_used[{fun, length(args_types)}], [])}
|
||||
else
|
||||
@@ -1384,7 +1438,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_apply({update_used?, info}, fun, args_types, expr, stack, context) do
|
||||
defp local_apply(update_used?, info, fun, args_types, expr, stack, context) do
|
||||
case local_apply(info, args_types, stack) do
|
||||
{indexes, type} ->
|
||||
context =
|
||||
@@ -1396,7 +1450,7 @@ defmodule Module.Types.Apply do
|
||||
context
|
||||
end
|
||||
|
||||
{type, context}
|
||||
{indexes, type, context}
|
||||
|
||||
:error ->
|
||||
error = {:badlocal, info, args_types, expr, context}
|
||||
@@ -1619,7 +1673,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
{message, to_trace, hints} =
|
||||
case reason do
|
||||
{:badarg, domain} ->
|
||||
{:badarg, domain, _empty?} ->
|
||||
message = """
|
||||
incompatible types given on #{mfa_or_call}:
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+450
-194
@@ -15,7 +15,6 @@ defmodule Module.Types.Expr do
|
||||
list_of_modules = list(atom())
|
||||
|
||||
@try_catch atom([:error, :exit, :throw])
|
||||
@atom_true atom([true])
|
||||
|
||||
@caller closed_map(
|
||||
__struct__: atom([Macro.Env]),
|
||||
@@ -35,13 +34,9 @@ defmodule Module.Types.Expr do
|
||||
versioned_vars: open_map()
|
||||
)
|
||||
|
||||
# An annotation for terms where the reverse arrow is not yet fully defined.
|
||||
# Also revisit all users of dynamic() in this module in a later date.
|
||||
@pending term()
|
||||
|
||||
# We do not make exception dynamic on purpose. If you do a blank rescue,
|
||||
# then we will assume you need to statically handle all possible exceptions.
|
||||
@exception open_map(__struct__: atom(), __exception__: @atom_true)
|
||||
@exception open_map(__struct__: atom(), __exception__: term())
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
|
||||
@@ -59,6 +54,9 @@ defmodule Module.Types.Expr do
|
||||
)
|
||||
)
|
||||
|
||||
@falsy atom([false, nil])
|
||||
@truthy negation(atom([false, nil]))
|
||||
|
||||
# :atom
|
||||
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
|
||||
do: {atom([atom]), context}
|
||||
@@ -139,7 +137,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_expr({:=, _, [left_expr, right_expr]} = match, expected, expr, stack, context) do
|
||||
def of_expr({:=, meta, [left_expr, right_expr]} = match, expected, expr, stack, context) do
|
||||
{left_expr, right_expr} = repack_match(left_expr, right_expr)
|
||||
|
||||
case left_expr do
|
||||
@@ -148,14 +146,20 @@ defmodule Module.Types.Expr do
|
||||
of_expr(right_expr, expected, expr, stack, context)
|
||||
|
||||
_ ->
|
||||
type_fun = fn pattern_type, context ->
|
||||
# See if we can use the expected type to further refine the pattern type,
|
||||
# if we cannot, use the pattern type as that will fail later on.
|
||||
{_ok_or_error, type} = compatible_intersection(dynamic(pattern_type), expected)
|
||||
of_expr(right_expr, type, expr, stack, context)
|
||||
end
|
||||
type_fun =
|
||||
fn pattern_type, context ->
|
||||
# See if we can use the expected type to further refine the pattern type,
|
||||
# if we cannot, use the pattern type as that will fail later on.
|
||||
type = intersection(pattern_type, expected)
|
||||
type = if empty?(type), do: pattern_type, else: type
|
||||
{result, context} = of_expr(right_expr, type, expr, stack, context)
|
||||
|
||||
Pattern.of_match(left_expr, type_fun, match, stack, context)
|
||||
# The function may still return a too broad type,
|
||||
# so we refine once again to assign the most appropriate to the pattern.
|
||||
{intersection(result, expected), context}
|
||||
end
|
||||
|
||||
Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -282,38 +286,159 @@ defmodule Module.Types.Expr do
|
||||
of_expr(post, expected, post, stack, context)
|
||||
end
|
||||
|
||||
def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected, expr, stack, original) do
|
||||
clauses
|
||||
|> reduce_non_empty({none(), original}, fn
|
||||
{:->, meta, [[head], body]}, {acc, context}, last? ->
|
||||
{head_type, context} = of_expr(head, @pending, head, stack, context)
|
||||
def of_expr(
|
||||
{:cond, meta,
|
||||
[
|
||||
[
|
||||
{:do,
|
||||
[
|
||||
{:->, pos_meta, [[pos_head], pos_body]},
|
||||
{:->, _neg_meta, [[neg_head], neg_body]}
|
||||
]}
|
||||
]
|
||||
]},
|
||||
expected,
|
||||
expr,
|
||||
stack,
|
||||
acc_context
|
||||
)
|
||||
when is_atom(neg_head) and neg_head not in [false, nil] do
|
||||
cache_result(meta, stack, acc_context, fn ->
|
||||
{pos_head_type, context} =
|
||||
of_expr(pos_head, term(), pos_head, %{stack | reverse_arrow: :cache}, acc_context)
|
||||
|
||||
context =
|
||||
if is_warning(stack) do
|
||||
case truthiness(head_type) do
|
||||
:always_true when not last? ->
|
||||
warning = {:badcond, "always match", head_type, head, context}
|
||||
warn(__MODULE__, warning, meta, stack, context)
|
||||
# Reset the context vars, keep warnings, as we will infer with expected truthy
|
||||
context = Of.reset_vars(context, acc_context)
|
||||
|
||||
:always_false ->
|
||||
warning = {:badcond, "never match", head_type, head, context}
|
||||
warn(__MODULE__, warning, meta, stack, context)
|
||||
context =
|
||||
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
|
||||
|
||||
_ ->
|
||||
context
|
||||
end
|
||||
else
|
||||
context
|
||||
end
|
||||
{_, truthy_context} =
|
||||
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
# Keep the context except the warnings, and compute the body
|
||||
context = reset_warnings(truthy_context, context)
|
||||
|
||||
{pos_body_type, context} =
|
||||
of_expr(pos_body, expected, expr, stack, context)
|
||||
|
||||
# Reset the context vars once again to compute the falsy type
|
||||
context = Of.reset_vars(context, acc_context)
|
||||
|
||||
{_, falsy_context} =
|
||||
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
context = reset_warnings(falsy_context, context)
|
||||
|
||||
{neg_body_type, context} =
|
||||
of_expr(neg_body, expected, expr, stack, context)
|
||||
|
||||
body_type = union(pos_body_type, neg_body_type)
|
||||
|
||||
context =
|
||||
cond do
|
||||
empty?(pos_body_type) -> Of.reset_vars(context, falsy_context)
|
||||
empty?(neg_body_type) -> Of.reset_vars(context, truthy_context)
|
||||
true -> Of.reset_vars(context, acc_context)
|
||||
end
|
||||
|
||||
dynamic_unless_static({body_type, context}, stack)
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
|
||||
def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, _expr, stack, context) do
|
||||
{case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context)
|
||||
def of_expr({:cond, meta, [[{:do, clauses}]]}, expected, expr, stack, context) do
|
||||
cache_result(meta, stack, context, fn ->
|
||||
{body_type, acc_context} =
|
||||
reduce_non_empty(clauses, {none(), context}, fn
|
||||
{:->, meta, [[head], body]}, {acc, acc_context}, last? ->
|
||||
{head_type, context} =
|
||||
of_expr(head, term(), head, %{stack | reverse_arrow: :cache}, acc_context)
|
||||
|
||||
# Reset the context vars, keep warnings, as we will infer with expected truthy
|
||||
context = Of.reset_vars(context, acc_context)
|
||||
|
||||
context =
|
||||
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
|
||||
|
||||
{_, truthy_context} =
|
||||
of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
# Keep the context except the warnings, and compute the body
|
||||
context = reset_warnings(truthy_context, context)
|
||||
|
||||
{body_type, context} =
|
||||
of_expr(body, expected, expr, stack, context)
|
||||
|
||||
# Reset the context vars once again to compute the falsy type
|
||||
context = Of.reset_vars(context, acc_context)
|
||||
|
||||
context =
|
||||
if last? do
|
||||
context
|
||||
else
|
||||
{_, falsy_context} =
|
||||
of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
reset_warnings(falsy_context, context)
|
||||
end
|
||||
|
||||
{union(body_type, acc), context}
|
||||
end)
|
||||
|
||||
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
|
||||
end)
|
||||
end
|
||||
|
||||
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
|
||||
when stack.reverse_arrow == :use do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
clauses = Map.fetch!(context.reverse_arrows, version)
|
||||
original = context
|
||||
|
||||
context =
|
||||
clauses
|
||||
|> Enum.reduce({0, []}, fn {_arg_type, body_type, _clause} = triplet, {counter, acc} ->
|
||||
if disjoint?(body_type, expected) do
|
||||
{counter + 1, acc}
|
||||
else
|
||||
{counter, [triplet | acc]}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
# Nothing skipped, just return the context as is
|
||||
{0, _} ->
|
||||
context
|
||||
|
||||
# If there is a single clause, we assume it is always evaluated
|
||||
# by doing reverse arrows and incorporting all variables into the context.
|
||||
# We have to evaluate the head again but it might have been preferred
|
||||
# if we could somehow merge a previously computed context.
|
||||
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
|
||||
{case_type, context} = of_expr(case_expr, arg_type, case_expr, stack, context)
|
||||
|
||||
{patterns, guards} = extract_head(head)
|
||||
previous = Pattern.init_previous()
|
||||
info = {{:case, meta, case_expr, case_type}, head}
|
||||
|
||||
{_, _, _, _, context} =
|
||||
Pattern.of_head(patterns, guards, [case_type], previous, info, meta, stack, context)
|
||||
|
||||
{_, context} = of_expr(body, expected, body, stack, context)
|
||||
reset_warnings(context, original)
|
||||
|
||||
{_, filtered} ->
|
||||
case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
|
||||
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
|
||||
reset_warnings(context, original)
|
||||
end
|
||||
|
||||
dynamic_unless_static({expected, context}, stack)
|
||||
end
|
||||
|
||||
def of_expr({:case, meta, [case_expr, [do: clauses]]}, expected, _expr, stack, base_context) do
|
||||
{case_type, context} =
|
||||
of_expr(case_expr, term(), case_expr, %{stack | reverse_arrow: :cache}, base_context)
|
||||
|
||||
info = {:case, meta, case_expr, case_type}
|
||||
|
||||
added_meta =
|
||||
@@ -326,146 +451,224 @@ defmodule Module.Types.Expr do
|
||||
# If the expression is generated or the construct is a literal,
|
||||
# it is most likely a macro code. However, if no clause is matched,
|
||||
# we should still check for that.
|
||||
if added_meta != [] do
|
||||
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
|
||||
else
|
||||
clauses
|
||||
end
|
||||
|> of_clauses([case_type], expected, info, stack, context, none())
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do
|
||||
[{:->, _, [head, _]} | _] = clauses
|
||||
{patterns, _guards} = extract_head(head)
|
||||
domain = Enum.map(patterns, fn _ -> dynamic() end)
|
||||
|
||||
{acc, context} =
|
||||
of_clauses_fun(clauses, domain, @pending, :fn, stack, context, [], fn
|
||||
trees, body, context, acc ->
|
||||
args_types = Pattern.of_domain(trees, stack, context)
|
||||
add_inferred(acc, args_types, body)
|
||||
end)
|
||||
|
||||
{fun_from_inferred_clauses(acc), context}
|
||||
end
|
||||
|
||||
def of_expr({:try, meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
|
||||
{after_block, blocks} = Keyword.pop(blocks, :after)
|
||||
{else_block, blocks} = Keyword.pop(blocks, :else)
|
||||
|
||||
{type, context} =
|
||||
if else_block do
|
||||
{type, context} = of_expr(body, @pending, body, stack, original)
|
||||
info = {:try_else, meta, body, type}
|
||||
of_clauses(else_block, [type], expected, info, stack, context, none())
|
||||
clauses =
|
||||
if added_meta != [] do
|
||||
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
|
||||
else
|
||||
of_expr(body, expected, expr, stack, original)
|
||||
clauses
|
||||
end
|
||||
|
||||
{type, context} =
|
||||
blocks
|
||||
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
|
||||
{:rescue, clauses}, acc_context ->
|
||||
Enum.reduce(clauses, acc_context, fn
|
||||
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
|
||||
{type, context} =
|
||||
of_rescue(var, exceptions, body, expr, :rescue, meta, stack, context)
|
||||
cache_arrows(meta, stack, fn ->
|
||||
acc = {false, none(), []}
|
||||
|
||||
{union(type, acc), context}
|
||||
{{none?, body_acc, clauses_acc}, context} =
|
||||
of_clauses_fun(clauses, [case_type], info, stack, context, acc, fn
|
||||
trees, precise?, {:->, _, [_, body]} = clause, context, acc ->
|
||||
# Compute the arg type based on the clause itself
|
||||
[arg_type] = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
{:->, meta, [[var], body]}, {acc, context} ->
|
||||
{type, context} =
|
||||
of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context)
|
||||
# Now we refine the case_expr context and use it to compute the body
|
||||
{_, refined_context} =
|
||||
of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
{union(type, acc), context}
|
||||
end)
|
||||
{body_type, context} =
|
||||
of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
|
||||
|
||||
{:catch, clauses}, {acc, context} ->
|
||||
args = [@try_catch, dynamic()]
|
||||
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
# Now we compute the return type and the clauses for reverse arrow
|
||||
{none?, body_acc, clauses_acc} = acc
|
||||
|
||||
if after_block do
|
||||
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
|
||||
{type, context}
|
||||
else
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
if precise? and empty?(body_type) do
|
||||
{{true, body_acc, clauses_acc}, context}
|
||||
else
|
||||
[arg_type] = Pattern.of_domain(trees, stack, context)
|
||||
clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
|
||||
{{none?, union(body_type, body_acc), clauses_acc}, context}
|
||||
end
|
||||
end)
|
||||
|
||||
@timeout_type union(integer(), atom([:infinity]))
|
||||
context =
|
||||
if none? or stack.mode != :static do
|
||||
head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
|
||||
|
||||
def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do
|
||||
blocks
|
||||
|> Enum.reduce({none(), original}, fn
|
||||
{:do, {:__block__, _, []}}, acc_context ->
|
||||
acc_context
|
||||
{_, refined_context} =
|
||||
of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
{:do, clauses}, {acc, context} ->
|
||||
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
|
||||
|
||||
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
|
||||
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
|
||||
if compatible?(timeout_type, @timeout_type) do
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
reset_warnings(refined_context, context)
|
||||
else
|
||||
error = {:badtimeout, timeout_type, timeout, context}
|
||||
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
|
||||
context
|
||||
end
|
||||
|
||||
{body_acc, clauses_acc, context}
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, meta, clauses}, _expected, _expr, stack, context) do
|
||||
cache_result(meta, stack, context, fn ->
|
||||
[{:->, _, [head, _]} | _] = clauses
|
||||
{patterns, _guards} = extract_head(head)
|
||||
domain = Enum.map(patterns, fn _ -> dynamic() end)
|
||||
|
||||
{acc, context} =
|
||||
of_clauses_fun(clauses, domain, :fn, stack, context, [], fn
|
||||
trees, _precise?, {:->, _, [_, body]}, context, acc ->
|
||||
{body_type, context} = of_expr(body, term(), body, stack, context)
|
||||
args_types = Pattern.of_domain(trees, stack, context)
|
||||
{add_inferred(acc, args_types, body_type), context}
|
||||
end)
|
||||
|
||||
{fun_from_inferred_clauses(acc), context}
|
||||
end)
|
||||
end
|
||||
|
||||
def of_expr({:try, meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
|
||||
cache_result(meta, stack, original, fn ->
|
||||
{after_block, blocks} = Keyword.pop(blocks, :after)
|
||||
{else_block, blocks} = Keyword.pop(blocks, :else)
|
||||
|
||||
{type, context} =
|
||||
if else_block do
|
||||
{type, context} = of_expr(body, term(), body, stack, original)
|
||||
info = {:try_else, type}
|
||||
of_clauses(else_block, [type], expected, info, stack, context, none())
|
||||
else
|
||||
of_expr(body, expected, expr, stack, original)
|
||||
end
|
||||
|
||||
{type, context} =
|
||||
blocks
|
||||
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
|
||||
{:rescue, clauses}, acc_context ->
|
||||
Enum.reduce(clauses, acc_context, fn
|
||||
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
|
||||
{type, context} =
|
||||
of_rescue(var, exceptions, body, expr, expected, :rescue, meta, stack, context)
|
||||
|
||||
{union(type, acc), context}
|
||||
|
||||
{:->, meta, [[var], body]}, {acc, context} ->
|
||||
{type, context} =
|
||||
of_rescue(var, [], body, var, expected, :anonymous_rescue, meta, stack, context)
|
||||
|
||||
{union(type, acc), context}
|
||||
end)
|
||||
|
||||
{:catch, clauses}, {acc, context} ->
|
||||
args = [@try_catch, dynamic()]
|
||||
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
|
||||
if after_block do
|
||||
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
|
||||
{type, context}
|
||||
else
|
||||
{type, context}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@timeout_type union(integer(), atom([:infinity]))
|
||||
|
||||
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
|
||||
cache_result(meta, stack, original, fn ->
|
||||
blocks
|
||||
|> Enum.reduce({none(), original}, fn
|
||||
{:do, {:__block__, _, []}}, acc_context ->
|
||||
acc_context
|
||||
|
||||
{:do, clauses}, {acc, context} ->
|
||||
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
|
||||
|
||||
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
|
||||
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
|
||||
if compatible?(timeout_type, @timeout_type) do
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
else
|
||||
error = {:badtimeout, timeout_type, timeout, context}
|
||||
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
|
||||
end
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
end)
|
||||
end
|
||||
|
||||
def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do
|
||||
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
|
||||
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
|
||||
cache_result(meta, stack, context, fn ->
|
||||
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
|
||||
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
|
||||
|
||||
# We don't need to type check uniq, as it is a compile-time boolean.
|
||||
# We handle reduce and into accordingly instead.
|
||||
if Keyword.has_key?(opts, :reduce) do
|
||||
reduce = Keyword.fetch!(opts, :reduce)
|
||||
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
|
||||
# TODO: We need to type check against dynamic() instead of using reduce_type
|
||||
# because this is recursive. We need to infer the block type first.
|
||||
args = [dynamic()]
|
||||
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
|
||||
else
|
||||
# TODO: Use the collectable protocol for the output
|
||||
into = Keyword.get(opts, :into, [])
|
||||
{into_type, into_kind, context} = for_into(into, meta, stack, context)
|
||||
{block_type, context} = of_expr(block, @pending, block, stack, context)
|
||||
# We don't need to type check uniq, as it is a compile-time boolean.
|
||||
# We handle reduce and into accordingly instead.
|
||||
if Keyword.has_key?(opts, :reduce) do
|
||||
reduce = Keyword.fetch!(opts, :reduce)
|
||||
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
|
||||
# TODO: We need to type check against dynamic() instead of using reduce_type
|
||||
# because this is recursive. We need to infer the block type first.
|
||||
args = [dynamic()]
|
||||
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
|
||||
else
|
||||
# TODO: Use the collectable protocol for the output
|
||||
into = Keyword.get(opts, :into, [])
|
||||
{into_type, into_kind, context} = for_into(into, meta, stack, context)
|
||||
|
||||
case into_kind do
|
||||
:bitstring ->
|
||||
case compatible_intersection(block_type, bitstring()) do
|
||||
{:ok, intersection} ->
|
||||
{return_union(into_type, intersection, stack), context}
|
||||
case into_kind do
|
||||
:bitstring ->
|
||||
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
|
||||
intersection = intersection(block_type, bitstring())
|
||||
|
||||
{:error, _} ->
|
||||
if empty?(intersection) do
|
||||
error = {:badbitbody, block_type, block, context}
|
||||
{error_type(), error(__MODULE__, error, meta, stack, context)}
|
||||
end
|
||||
else
|
||||
{union(into_type, intersection), context}
|
||||
end
|
||||
|
||||
:non_empty_list ->
|
||||
{return_union(into_type, non_empty_list(block_type), stack), context}
|
||||
:non_empty_list ->
|
||||
expected =
|
||||
case list_hd(expected) do
|
||||
{:ok, head} -> head
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
:none ->
|
||||
{into_type, context}
|
||||
{block_type, context} = of_expr(block, expected, block, stack, context)
|
||||
{union(into_type, non_empty_list(block_type)), context}
|
||||
|
||||
:none ->
|
||||
{_, context} = of_expr(block, term(), block, stack, context)
|
||||
{into_type, context}
|
||||
end
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# TODO: with pat <- expr do expr end
|
||||
def of_expr({:with, _meta, [_ | _] = clauses}, _expected, _expr, stack, original) do
|
||||
{clauses, [options]} = Enum.split(clauses, -1)
|
||||
context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2))
|
||||
context = Enum.reduce(options, context, &with_option(&1, stack, &2, original))
|
||||
{dynamic(), context}
|
||||
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
|
||||
cache_result(meta, stack, original, fn ->
|
||||
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
|
||||
|
||||
{else_types, context} =
|
||||
Enum.reduce(clauses, {none(), original}, &with_clause(&1, stack, &2))
|
||||
|
||||
{do_result, context} = of_expr(do_block, expected, do_block, stack, context)
|
||||
context = Of.reset_vars(context, original)
|
||||
|
||||
{else_result, context} =
|
||||
case options do
|
||||
[else: clauses] ->
|
||||
info = {:with_else, else_types}
|
||||
of_clauses(clauses, [else_types], expected, info, stack, context, none())
|
||||
|
||||
[] ->
|
||||
{else_types, context}
|
||||
end
|
||||
|
||||
dynamic_unless_static({union(do_result, else_result), context}, stack)
|
||||
end)
|
||||
end
|
||||
|
||||
def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do
|
||||
@@ -473,7 +676,7 @@ defmodule Module.Types.Expr do
|
||||
|
||||
# TODO: Perform inference based on the strong domain of a function
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
|
||||
Enum.map_reduce(args, context, &of_expr(&1, term(), &1, stack, &2))
|
||||
|
||||
Apply.fun(fun_type, args_types, call, stack, context)
|
||||
end
|
||||
@@ -557,8 +760,8 @@ defmodule Module.Types.Expr do
|
||||
|
||||
## Try
|
||||
|
||||
defp of_rescue(var, exceptions, body, expr, info, meta, stack, original) do
|
||||
args = [__exception__: @atom_true]
|
||||
defp of_rescue(var, exceptions, body, expr, expected, info, meta, stack, original) do
|
||||
args = [__exception__: term()]
|
||||
|
||||
{structs, context} =
|
||||
Enum.map_reduce(exceptions, original, fn exception, context ->
|
||||
@@ -586,26 +789,32 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
end
|
||||
|
||||
{type, context} = of_expr(body, @pending, body, stack, context)
|
||||
{type, context} = of_expr(body, expected, body, stack, context)
|
||||
{type, Of.reset_vars(context, original)}
|
||||
end
|
||||
|
||||
## Comprehensions
|
||||
|
||||
defp for_clause({:<-, meta, [left, right]}, stack, context) do
|
||||
expr = {:<-, [type_check: :generator] ++ meta, [left, right]}
|
||||
{pattern, guards} = extract_head([left])
|
||||
meta = [type_check: :generator] ++ meta
|
||||
expr = {:<-, meta, [left, right]}
|
||||
{[pattern], guards} = extract_head([left])
|
||||
|
||||
# TODO: Extract the type from enumerable protocol
|
||||
{_type, context} =
|
||||
Apply.remote(Enumerable, :count, [right], dynamic(), expr, stack, context, &of_expr/5)
|
||||
Apply.remote(Enumerable, :count, [right], term(), expr, stack, context, &of_expr/5)
|
||||
|
||||
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context)
|
||||
{_tree, _precise, context} =
|
||||
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, meta, stack, context)
|
||||
|
||||
context
|
||||
end
|
||||
|
||||
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do
|
||||
{right_type, context} = of_expr(right, bitstring(), expr, stack, context)
|
||||
info = {:for, expr, dynamic()}
|
||||
context = Pattern.of_generator(left, [], bitstring(), info, expr, stack, context)
|
||||
|
||||
{_tree, _precise?, context} =
|
||||
Pattern.of_generator(left, [], bitstring(), :for, expr, meta, stack, context)
|
||||
|
||||
if compatible?(right_type, bitstring()) do
|
||||
context
|
||||
@@ -666,34 +875,29 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
end
|
||||
|
||||
defp return_union(left, right, stack) do
|
||||
Apply.return(union(left, right), [left, right], stack)
|
||||
end
|
||||
|
||||
## With
|
||||
|
||||
defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do
|
||||
{pattern, guards} = extract_head([left])
|
||||
{_type, context} = of_expr(right, @pending, right, stack, context)
|
||||
info = {:with, expr, dynamic()}
|
||||
Pattern.of_generator(pattern, guards, dynamic(), info, expr, stack, context)
|
||||
defp with_clause({:<-, meta, [left, right]} = expr, stack, {else_types, context}) do
|
||||
{[pattern], guards} = extract_head([left])
|
||||
|
||||
{type, context} =
|
||||
of_expr(right, term(), right, %{stack | reverse_arrow: :cache}, context)
|
||||
|
||||
{trees, precise?, context} =
|
||||
Pattern.of_generator(pattern, guards, type, :with, expr, meta, stack, context)
|
||||
|
||||
[pattern_type] = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
{_, refined_context} =
|
||||
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
|
||||
|
||||
else_type = if precise?, do: difference(type, pattern_type), else: type
|
||||
{union(else_type, else_types), reset_warnings(refined_context, context)}
|
||||
end
|
||||
|
||||
defp with_clause(expr, stack, context) do
|
||||
{_type, context} = of_expr(expr, @pending, expr, stack, context)
|
||||
context
|
||||
end
|
||||
|
||||
defp with_option({:do, body}, stack, context, original) do
|
||||
{_type, context} = of_expr(body, @pending, body, stack, context)
|
||||
Of.reset_vars(context, original)
|
||||
end
|
||||
|
||||
defp with_option({:else, clauses}, stack, context, _original) do
|
||||
{_, context} =
|
||||
of_clauses(clauses, [dynamic()], @pending, :with_else, stack, context, none())
|
||||
|
||||
context
|
||||
defp with_clause(expr, stack, {else_types, context}) do
|
||||
{_type, context} = of_expr(expr, term(), expr, stack, context)
|
||||
{else_types, context}
|
||||
end
|
||||
|
||||
## General helpers
|
||||
@@ -722,36 +926,88 @@ defmodule Module.Types.Expr do
|
||||
defp reduce_non_empty([head | tail], acc, fun),
|
||||
do: reduce_non_empty(tail, fun.(head, acc, false), fun)
|
||||
|
||||
defp maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?) do
|
||||
if is_warning(stack) do
|
||||
case truthiness(head_type) do
|
||||
:always_true when not last? ->
|
||||
warning = {:badcond, "always match", head_type, head, context}
|
||||
warn(__MODULE__, warning, meta, stack, context)
|
||||
|
||||
:always_false ->
|
||||
warning = {:badcond, "never match", head_type, head, context}
|
||||
warn(__MODULE__, warning, meta, stack, context)
|
||||
|
||||
_ ->
|
||||
context
|
||||
end
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
|
||||
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
|
||||
|
||||
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
|
||||
fun = fn _args_types, result, _context, acc -> union(result, acc) end
|
||||
of_clauses_fun(clauses, domain, expected, base_info, stack, context, acc, fun)
|
||||
defp cache_result(meta, %{reverse_arrow: reverse_arrow}, context, fun) do
|
||||
case reverse_arrow do
|
||||
nil ->
|
||||
fun.()
|
||||
|
||||
:cache ->
|
||||
{result, context} = fun.()
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
context = put_in(context.reverse_arrows[version], result)
|
||||
{result, context}
|
||||
|
||||
:use ->
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
{Map.fetch!(context.reverse_arrows, version), context}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_clauses_fun(clauses, domain, expected, base_info, stack, original, acc, fun) do
|
||||
defp cache_arrows(_meta, %{reverse_arrow: nil}, fun) do
|
||||
{result, _cache, context} = fun.()
|
||||
{result, context}
|
||||
end
|
||||
|
||||
defp cache_arrows(meta, %{reverse_arrow: :cache}, fun) do
|
||||
{result, cache, context} = fun.()
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
context = put_in(context.reverse_arrows[version], cache)
|
||||
{result, context}
|
||||
end
|
||||
|
||||
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
|
||||
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
|
||||
{body_type, context} = of_expr(body, expected, body, stack, context)
|
||||
{union(acc, body_type), context}
|
||||
end
|
||||
|
||||
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
|
||||
end
|
||||
|
||||
defp of_clauses_fun(clauses, domain, base_info, stack, original, acc, of_acc) do
|
||||
%{failed: failed?} = original
|
||||
|
||||
{result, _previous, context} =
|
||||
Enum.reduce(clauses, {acc, Pattern.init_previous(), original}, fn
|
||||
{:->, meta, [head, body]}, {acc, previous, context} ->
|
||||
{:->, meta, [head, _]} = clause, {acc, previous, context} ->
|
||||
{failed?, context} = reset_failed(context, failed?)
|
||||
{patterns, guards} = extract_head(head)
|
||||
info = {base_info, head}
|
||||
|
||||
{trees, previous, context} =
|
||||
{trees, precise?, _, previous, context} =
|
||||
Pattern.of_head(patterns, guards, domain, previous, info, meta, stack, context)
|
||||
|
||||
{result, context} = of_expr(body, expected, body, stack, context)
|
||||
|
||||
{fun.(trees, result, context, acc), previous,
|
||||
context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
{acc, context} = of_acc.(trees, precise?, clause, context, acc)
|
||||
{acc, previous, context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
end)
|
||||
|
||||
{result, context}
|
||||
end
|
||||
|
||||
defp reset_warnings(context, %{warnings: warnings}), do: %{context | warnings: warnings}
|
||||
|
||||
defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}}
|
||||
defp reset_failed(context, _), do: {false, context}
|
||||
|
||||
|
||||
@@ -495,7 +495,7 @@ defmodule Module.Types.Of do
|
||||
def struct_info(struct, kind, meta, stack, context) do
|
||||
case stack.no_warn_undefined do
|
||||
%Macro.Env{} = env ->
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, env) do
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
|
||||
{:ok, info} -> {info, context}
|
||||
{:error, _desc} -> {nil, context}
|
||||
end
|
||||
|
||||
@@ -18,6 +18,7 @@ defmodule Module.Types.Pattern do
|
||||
defp empty_previous?({[], _}), do: true
|
||||
defp empty_previous?({[_ | _], _}), do: false
|
||||
|
||||
# Previous is always an upper bound (static), so we can use subtype?
|
||||
defp previous_subtype?(_, {[], _}), do: false
|
||||
defp previous_subtype?([], _), do: true
|
||||
defp previous_subtype?(args, {_, descr}), do: subtype?(args_to_previous(args), descr)
|
||||
@@ -49,7 +50,9 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
|
||||
defp previous_to_string({list, _}) do
|
||||
Enum.map_join(list, "\n ", fn types ->
|
||||
list
|
||||
|> Enum.reverse()
|
||||
|> Enum.map_join("\n ", fn types ->
|
||||
types
|
||||
|> Enum.map_join(", ", &(&1 |> upper_bound() |> to_quoted_string()))
|
||||
|> indent(4)
|
||||
@@ -189,27 +192,28 @@ defmodule Module.Types.Pattern do
|
||||
# First we check if it fails without previous, if it doesn't, check if it is redundant.
|
||||
case of_precise_head(patterns, guards, expected, init_previous(), tag, stack, original) do
|
||||
{other_trees, _, _, _, %{failed: true} = other_context} ->
|
||||
{other_trees, previous, other_context}
|
||||
{other_trees, false, args_types, previous, other_context}
|
||||
|
||||
{other_trees, _, _, args_types, other_context} ->
|
||||
if previous_subtype?(args_types, previous) do
|
||||
warning = {:redundant, tag, expected, args_types, previous, other_context}
|
||||
{other_trees, previous, warn(__MODULE__, warning, meta, stack, other_context)}
|
||||
context = warn(__MODULE__, warning, meta, stack, other_context)
|
||||
{other_trees, false, args_types, previous, context}
|
||||
else
|
||||
{trees, previous, context}
|
||||
{trees, false, args_types, previous, context}
|
||||
end
|
||||
end
|
||||
else
|
||||
cond do
|
||||
check_previous? and previous_subtype?(args_types, previous) ->
|
||||
warning = {:redundant, tag, expected, args_types, previous, context}
|
||||
{trees, previous, warn(__MODULE__, warning, meta, stack, context)}
|
||||
{trees, false, args_types, previous, warn(__MODULE__, warning, meta, stack, context)}
|
||||
|
||||
precise? ->
|
||||
{trees, concat_previous(args_types, previous), context}
|
||||
{trees, true, args_types, concat_previous(args_types, previous), context}
|
||||
|
||||
true ->
|
||||
{trees, previous, context}
|
||||
{trees, false, args_types, previous, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -234,16 +238,15 @@ defmodule Module.Types.Pattern do
|
||||
of_pattern_intersect(trees, 0, [], pattern_info, tag, stack, context),
|
||||
# We compute the args types before we do the intersection with previous clauses
|
||||
args_types =
|
||||
(with [_ | _] <- previous,
|
||||
{:ok, _types, context} <-
|
||||
(with false <- empty_previous?(previous),
|
||||
{:ok, context} <-
|
||||
of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
|
||||
trees_to_args_types(trees, stack, context)
|
||||
else
|
||||
_ -> nil
|
||||
end),
|
||||
{types, check_previous?} = of_pattern_previous(types, previous, stack),
|
||||
{:ok, _types, context} <-
|
||||
of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
|
||||
{:ok, context} <- of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
|
||||
{trees, pattern_precise? and guard_precise?, check_previous?,
|
||||
args_types || trees_to_args_types(trees, stack, context), context}
|
||||
else
|
||||
@@ -261,18 +264,13 @@ defmodule Module.Types.Pattern do
|
||||
@doc """
|
||||
Computes the domain from the pattern tree and expected types.
|
||||
|
||||
Note we use `upper_bound` because the user of dynamic in the signature
|
||||
Note we use `upper_bound` because the use of dynamic in the signature
|
||||
won't make a difference.
|
||||
"""
|
||||
def of_domain([{tree, expected, _pattern} | trees], stack, context) do
|
||||
[
|
||||
intersection(of_pattern_tree(tree, stack, context), expected) |> upper_bound()
|
||||
| of_domain(trees, stack, context)
|
||||
]
|
||||
end
|
||||
|
||||
def of_domain([], _stack, _context) do
|
||||
[]
|
||||
def of_domain(trees, stack, context) do
|
||||
Enum.map(trees, fn {tree, _, _} ->
|
||||
tree |> of_pattern_tree(stack, context) |> upper_bound()
|
||||
end)
|
||||
end
|
||||
|
||||
defp of_pattern_args_zip(
|
||||
@@ -298,7 +296,8 @@ defmodule Module.Types.Pattern do
|
||||
@doc """
|
||||
Handles the match operator.
|
||||
"""
|
||||
def of_match(pattern, expected_fun, expr, stack, context) do
|
||||
def of_match(pattern, expected_fun, expr, meta, stack, context) do
|
||||
stack = %{stack | meta: meta}
|
||||
context = init_pattern_info(context, [])
|
||||
|
||||
{tree, _precise?, context} =
|
||||
@@ -312,9 +311,9 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
with {:ok, types} <-
|
||||
of_pattern_intersect(args, 0, [], pattern_info, tag, stack, context),
|
||||
{:ok, [type], context} <-
|
||||
{:ok, context} <-
|
||||
of_pattern_refine(types, %{}, pattern_info, tag, stack, context) do
|
||||
{type, context}
|
||||
{of_pattern_tree(tree, stack, context), context}
|
||||
else
|
||||
{:error, context} -> {expected, context}
|
||||
end
|
||||
@@ -323,23 +322,27 @@ defmodule Module.Types.Pattern do
|
||||
@doc """
|
||||
Handles matches in generators.
|
||||
"""
|
||||
def of_generator(pattern, guards, expected, tag, expr, stack, %{vars: vars} = context) do
|
||||
def of_generator(pattern, guards, expected, op, expr, meta, stack, %{vars: vars} = context)
|
||||
when is_atom(op) do
|
||||
stack = %{stack | meta: meta}
|
||||
context = init_pattern_info(context, [])
|
||||
|
||||
{tree, _precise?, context} =
|
||||
{tree, pattern_precise?, context} =
|
||||
of_pattern(pattern, [%{root: {:arg, 0}, expr: expr}], stack, context)
|
||||
|
||||
{pattern_info, context} = pop_pattern_info(context)
|
||||
{guard_precise?, changed, context} = of_guards(guards, vars, stack, context)
|
||||
|
||||
args = [{tree, expected, pattern}]
|
||||
{_precise?, changed, context} = of_guards(guards, vars, stack, context)
|
||||
tag = {op, expr, expected}
|
||||
|
||||
with {:ok, types} <-
|
||||
of_pattern_intersect(args, 0, [], pattern_info, tag, stack, context),
|
||||
{:ok, _types, context} <-
|
||||
{:ok, context} <-
|
||||
of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
|
||||
context
|
||||
{args, pattern_precise? and guard_precise?, context}
|
||||
else
|
||||
{:error, context} -> context
|
||||
{:error, context} -> {args, false, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -398,7 +401,7 @@ defmodule Module.Types.Pattern do
|
||||
context -> {:error, error_vars(pattern_info, context)}
|
||||
else
|
||||
{changed, context} ->
|
||||
{:ok, types, of_changed(Map.keys(changed), stack, context)}
|
||||
{:ok, of_changed(Map.keys(changed), stack, context)}
|
||||
end
|
||||
|
||||
defp error_vars(pattern_info, context) do
|
||||
@@ -410,9 +413,11 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
defp match_var do
|
||||
version = make_ref()
|
||||
{version, {:match, [version: version], __MODULE__}}
|
||||
{version, match_var(version)}
|
||||
end
|
||||
|
||||
defp match_var(version), do: {:match, [version: version], __MODULE__}
|
||||
|
||||
defp match_error?({:match, _, __MODULE__}, _type), do: true
|
||||
defp match_error?(_var, type), do: empty?(type)
|
||||
|
||||
@@ -805,12 +810,7 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
end
|
||||
|
||||
# _
|
||||
defp of_pattern({:_, _meta, _var_context}, _path, _stack, context) do
|
||||
{term(), true, context}
|
||||
end
|
||||
|
||||
# var
|
||||
# var (includes underscores)
|
||||
defp of_pattern({name, meta, ctx} = var, path, _stack, context)
|
||||
when is_atom(name) and is_atom(ctx) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
@@ -1023,8 +1023,8 @@ defmodule Module.Types.Pattern do
|
||||
guard_context: :andalso,
|
||||
parent_version: nil,
|
||||
vars: vars,
|
||||
changed: %{},
|
||||
subpatterns: %{}
|
||||
subpatterns_vars: %{},
|
||||
changed: %{}
|
||||
})
|
||||
|
||||
{precise?, context} = of_guards(guards, stack, context)
|
||||
@@ -1049,7 +1049,12 @@ defmodule Module.Types.Pattern do
|
||||
end)
|
||||
|
||||
expr = Enum.reduce(guards, {:_, [], []}, &{:when, [], [&2, &1]})
|
||||
context = %{context | vars: vars, conditional_vars: conditional_vars}
|
||||
|
||||
context = %{
|
||||
context
|
||||
| vars: Map.merge(vars, context.pattern_info.subpatterns_vars),
|
||||
conditional_vars: conditional_vars
|
||||
}
|
||||
|
||||
{precise? and Of.all_same_conditional_vars?(vars_conds),
|
||||
Of.reduce_conditional_vars(vars_conds, expr, stack, context)}
|
||||
@@ -1070,7 +1075,7 @@ defmodule Module.Types.Pattern do
|
||||
{false, context}
|
||||
|
||||
{true, maybe_or_always} ->
|
||||
{maybe_or_always == :always and context.pattern_info.subpatterns == %{}, context}
|
||||
{maybe_or_always == :always, context}
|
||||
|
||||
_false_tuple_or_none ->
|
||||
error = {:badguard, type, guard, context}
|
||||
@@ -1261,12 +1266,12 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
defp of_remote(fun, _args, call, expected, stack, context)
|
||||
when fun in [:and, :or, :andalso, :orelse] do
|
||||
{both_domain, abort_domain, always_rhs?} =
|
||||
{boolean, abort_domain, always_rhs?} =
|
||||
case fun do
|
||||
:andalso -> {@atom_true, @atom_false, false}
|
||||
:orelse -> {@atom_false, @atom_true, false}
|
||||
:and -> {@atom_true, @atom_false, true}
|
||||
:or -> {@atom_false, @atom_true, true}
|
||||
:andalso -> {true, @atom_false, false}
|
||||
:orelse -> {false, @atom_true, false}
|
||||
:and -> {true, @atom_false, true}
|
||||
:or -> {false, @atom_true, true}
|
||||
end
|
||||
|
||||
# If we have multiple operations in a row,
|
||||
@@ -1282,8 +1287,8 @@ defmodule Module.Types.Pattern do
|
||||
# only be true if both clauses are executed, so we know the first
|
||||
# argument has to be true and the second has to be expected.
|
||||
cond do
|
||||
subtype?(expected, both_domain) ->
|
||||
of_logical_all([left | right], true, both_domain, abort_domain, stack, context)
|
||||
is_nil(context.pattern_info) or booleaness(expected) == {boolean, :always} ->
|
||||
of_logical_all([left | right], true, expected, abort_domain, stack, context)
|
||||
|
||||
right == [] ->
|
||||
of_guard(left, expected, left, stack, context)
|
||||
@@ -1329,7 +1334,7 @@ defmodule Module.Types.Pattern do
|
||||
call,
|
||||
expected,
|
||||
stack,
|
||||
%{pattern_info: %{subpatterns: subpatterns}} = context
|
||||
context
|
||||
)
|
||||
when fun in [:hd, :tl] do
|
||||
arg_key =
|
||||
@@ -1340,18 +1345,38 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
subpattern_key = {fun, arg_key}
|
||||
|
||||
{var, context} =
|
||||
case subpatterns do
|
||||
%{^subpattern_key => var} ->
|
||||
{var, context}
|
||||
{found?, var_version, context} =
|
||||
case context.subpatterns do
|
||||
%{^subpattern_key => var_version} ->
|
||||
case context.vars do
|
||||
%{^var_version => _} -> {true, var_version, context}
|
||||
%{} -> {false, var_version, context}
|
||||
end
|
||||
|
||||
%{} ->
|
||||
{type, context} =
|
||||
Apply.remote(:erlang, fun, [arg], expected, call, stack, context, &of_guard/5)
|
||||
var_version = make_ref()
|
||||
{false, var_version, put_in(context.subpatterns[subpattern_key], var_version)}
|
||||
end
|
||||
|
||||
{_, var} = match_var()
|
||||
context = Of.declare_var(var, type, context)
|
||||
{var, put_in(context.pattern_info.subpatterns[subpattern_key], var)}
|
||||
var = match_var(var_version)
|
||||
|
||||
context =
|
||||
if found? do
|
||||
context
|
||||
else
|
||||
{type, context} =
|
||||
Apply.remote(:erlang, fun, [arg], expected, call, stack, context, &of_guard/5)
|
||||
|
||||
context = Of.declare_var(var, type, context)
|
||||
|
||||
update_in(context.pattern_info, fn %{subpatterns_vars: subpatterns_vars} = pattern_info ->
|
||||
%{
|
||||
pattern_info
|
||||
| vars: false,
|
||||
subpatterns_vars:
|
||||
Map.put(subpatterns_vars, var_version, Map.fetch!(context.vars, var_version))
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
of_guard(var, expected, call, stack, context)
|
||||
@@ -1586,8 +1611,9 @@ defmodule Module.Types.Pattern do
|
||||
#
|
||||
# $ typep head_pattern =
|
||||
# {{:def, kind, fun, types}, args, guards} or
|
||||
# {{:case | :try_else, meta, expr, type}, [arg]} or
|
||||
# {:for_reduce | :receive | :try_catch | :with_else | :fn, [arg]}
|
||||
# {{:case, meta, expr, type}, [arg]} or
|
||||
# {{:try_else | :with_else, type}, [arg]} or
|
||||
# {:for_reduce | :receive | :try_catch | :fn, [arg]}
|
||||
#
|
||||
# $ typep match_pattern =
|
||||
# {:with or :for or :match, pattern, type}
|
||||
@@ -1633,62 +1659,63 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
end
|
||||
|
||||
defp badpattern({{op, meta, expr, type}, args}, _index) when op in [:case, :try_else] do
|
||||
with {:case, op} <- meta[:type_check] do
|
||||
message =
|
||||
cond do
|
||||
op == :|| ->
|
||||
additional =
|
||||
with {:case, meta, [_, _]} <- expr,
|
||||
{:case, :||} <- meta[:type_check] do
|
||||
"(shown as ... below) "
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
defp badpattern({{:case, meta, expr, type}, args}, _index) do
|
||||
case meta[:type_check] do
|
||||
{:case, op} ->
|
||||
message =
|
||||
cond do
|
||||
op == :|| ->
|
||||
additional =
|
||||
with {:case, meta, [_, _]} <- expr,
|
||||
{:case, :||} <- meta[:type_check] do
|
||||
"(shown as ... below) "
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
|
||||
"""
|
||||
the right-hand side of || #{additional}will never be executed:
|
||||
"""
|
||||
the right-hand side of || #{additional}will never be executed:
|
||||
|
||||
#{expr_to_string({:||, [], [expr, {:..., [], []}]}) |> indent(4)}
|
||||
#{expr_to_string({:||, [], [expr, {:..., [], []}]}) |> indent(4)}
|
||||
|
||||
because the left-hand side always evaluates to:
|
||||
because the left-hand side always evaluates to:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
|
||||
op in [:and, :or] ->
|
||||
{first_message, second_message} =
|
||||
case booleaness(type) do
|
||||
{true, _} -> {" will always succeed", "because it evaluates to"}
|
||||
{false, _} -> {" will never succeed", "because it evaluates to"}
|
||||
:none -> {" will always fail", "because it evaluates to"}
|
||||
_ -> {"", "will always evaluate to"}
|
||||
end
|
||||
op in [:and, :or] ->
|
||||
{first_message, second_message} =
|
||||
case booleaness(type) do
|
||||
{true, _} -> {" will always succeed", "because it evaluates to"}
|
||||
{false, _} -> {" will never succeed", "because it evaluates to"}
|
||||
:none -> {" will always fail", "because it evaluates to"}
|
||||
_ -> {"", "will always evaluate to"}
|
||||
end
|
||||
|
||||
"""
|
||||
the following conditional expression#{first_message}:
|
||||
"""
|
||||
the following conditional expression#{first_message}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
#{second_message}:
|
||||
#{second_message}:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
|
||||
true ->
|
||||
"""
|
||||
the following conditional expression:
|
||||
true ->
|
||||
"""
|
||||
the following conditional expression:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
will always evaluate to:
|
||||
will always evaluate to:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
end
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
end
|
||||
|
||||
{expr, message}
|
||||
|
||||
{expr, message}
|
||||
else
|
||||
_ ->
|
||||
{args,
|
||||
"""
|
||||
@@ -1707,8 +1734,22 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
end
|
||||
|
||||
defp badpattern({op, args}, index)
|
||||
when op in [:for_reduce, :receive, :try_catch, :with_else, :fn] do
|
||||
defp badpattern({{op, type}, args}, index) when op in [:try_else, :with_else] do
|
||||
arg = Enum.fetch!(args, index)
|
||||
|
||||
{arg,
|
||||
"""
|
||||
the following clause will never match:
|
||||
|
||||
#{args_to_string(args) |> indent(4)} ->
|
||||
|
||||
it is expected to match on type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""}
|
||||
end
|
||||
|
||||
defp badpattern({op, args}, index) when op in [:for_reduce, :receive, :try_catch, :fn] do
|
||||
arg = Enum.fetch!(args, index)
|
||||
|
||||
{arg,
|
||||
|
||||
@@ -952,8 +952,14 @@ defmodule OptionParser do
|
||||
|
||||
case types |> List.delete(:keep) |> List.first(:string) do
|
||||
:boolean ->
|
||||
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
case to_switch(name) do
|
||||
"--no-" <> _ = switch ->
|
||||
add_aliases(switch, name, reverse_aliases)
|
||||
|
||||
switch ->
|
||||
base = "#{switch}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
end
|
||||
|
||||
type ->
|
||||
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
|
||||
|
||||
@@ -485,7 +485,7 @@ defmodule Path do
|
||||
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
|
||||
case :file.get_cwd() do
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
|
||||
_ -> path
|
||||
_ -> IO.chardata_to_string(path)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -786,7 +786,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp fallback_clause_for(value, _protocol, meta) do
|
||||
{meta, [quote(do: _)], [], value}
|
||||
{meta, [{:_, [version: -1], __MODULE__}], [], value}
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
|
||||
+191
-76
@@ -258,7 +258,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{pid, _} ->
|
||||
if Process.alive?(pid), do: pid, else: :undefined
|
||||
|
||||
@@ -369,7 +369,10 @@ defmodule Registry do
|
||||
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
|
||||
many different keys (e.g., many topics with few subscribers each). This makes
|
||||
key-based lookups more efficient as they only need to check a single partition
|
||||
instead of all partitions.
|
||||
instead of all partitions. This option uses a different internal ETS table type
|
||||
(`ordered_set` instead of `duplicate_bag`), which means match specs passed to
|
||||
`select/2` and `count_select/2` that reference `:"$_"` may behave differently.
|
||||
Use named match variables (`:"$1"`, `:"$2"`, etc.) instead.
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@@ -532,13 +535,17 @@ defmodule Registry do
|
||||
given as an option, the dispatching happens in parallel. In both cases,
|
||||
the callback is only invoked if there are entries for that partition.
|
||||
|
||||
For `{:duplicate, :key}` registries, all entries for a given key are in a
|
||||
single partition, so the `:parallel` option has no effect.
|
||||
|
||||
See the module documentation for examples of using the `dispatch/3`
|
||||
function for building custom dispatching or a pubsub system.
|
||||
|
||||
## Options
|
||||
|
||||
* `:parallel` - if `true`, the dispatching is done in parallel
|
||||
across all partitions. Defaults to `false`.
|
||||
across all partitions. Only meaningful for `{:duplicate, :pid}`
|
||||
registries. Defaults to `false`.
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@@ -549,17 +556,24 @@ defmodule Registry do
|
||||
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
(key_ets || key_ets!(registry, key, partitions))
|
||||
|> safe_lookup_second(key)
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
:unique
|
||||
|> lookup_second(key_ets, key)
|
||||
|> List.wrap()
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
key_ets
|
||||
|> safe_lookup_second(key)
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
kind
|
||||
|> lookup_second(key_ets, key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, _}, partitions, _} ->
|
||||
{{:duplicate, :key}, partitions, _} ->
|
||||
{:duplicate, :key}
|
||||
|> lookup_second(key_ets!(registry, key, partitions), key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, :pid}, partitions, _} ->
|
||||
if Keyword.get(opts, :parallel, false) do
|
||||
registry
|
||||
|> dispatch_parallel(key, mfa_or_fun, partitions)
|
||||
@@ -579,9 +593,8 @@ defmodule Registry do
|
||||
defp dispatch_serial(registry, key, mfa_or_fun, partition) do
|
||||
partition = partition - 1
|
||||
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
{:duplicate, :pid}
|
||||
|> lookup_second(key_ets!(registry, partition), key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
dispatch_serial(registry, key, mfa_or_fun, partition)
|
||||
@@ -597,9 +610,8 @@ defmodule Registry do
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
{:duplicate, :pid}
|
||||
|> lookup_second(key_ets!(registry, partition), key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
Process.unlink(parent)
|
||||
@@ -663,7 +675,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{_, _} = pair ->
|
||||
[pair]
|
||||
|
||||
@@ -671,16 +683,15 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
safe_lookup_second(key_ets, key)
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
lookup_second(kind, key_ets, key)
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
partition = hash(key, partitions)
|
||||
safe_lookup_second(key_ets!(registry, partition), key)
|
||||
lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key)
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- safe_lookup_second(key_ets!(registry, partition), key),
|
||||
pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key),
|
||||
do: pair
|
||||
end
|
||||
end
|
||||
@@ -791,21 +802,18 @@ defmodule Registry do
|
||||
@doc since: "1.4.0"
|
||||
@spec match(registry, key, match_pattern, guards) :: [{pid, term}]
|
||||
def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select(key_ets, spec)
|
||||
{{:duplicate, :pid}, partitions, nil} ->
|
||||
spec = match_spec({:duplicate, :pid}, key, pattern, guards)
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
:ets.select(key_ets, spec)
|
||||
|
||||
{{:duplicate, _}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- :ets.select(key_ets!(registry, partition), spec),
|
||||
do: pair
|
||||
|
||||
{kind, partitions, key_ets} ->
|
||||
spec = match_spec(kind, key, pattern, guards)
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -943,7 +951,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{^pid, value} ->
|
||||
[value]
|
||||
|
||||
@@ -951,18 +959,17 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
for {^pid, value} <- lookup_second(kind, key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
partition = hash(key, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
key_ets = key_ets!(registry, key, partitions)
|
||||
for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
partition = hash(pid, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1016,7 +1023,8 @@ defmodule Registry do
|
||||
# Remove first from the key_ets because in case of crashes
|
||||
# the pid_ets will still be able to clean up. The last step is
|
||||
# to clean if we have no more entries.
|
||||
true = __unregister__(key_ets, {key, {self, :_}}, 1)
|
||||
true = __unregister__(key_ets, {key, {self, :_}}, 1, ordered?(kind))
|
||||
|
||||
true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2)
|
||||
|
||||
unlink_if_unregistered(pid_server, pid_ets, self)
|
||||
@@ -1081,13 +1089,10 @@ defmodule Registry do
|
||||
# to clean if we have no more entries.
|
||||
|
||||
# Here we want to count all entries for this pid under this key, regardless of pattern.
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
|
||||
{total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards)
|
||||
total = :ets.select_count(key_ets, total_spec)
|
||||
|
||||
# We only want to delete things that match the pattern
|
||||
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]
|
||||
|
||||
case :ets.select_delete(key_ets, delete_spec) do
|
||||
# We deleted everything, we can just delete the object
|
||||
^total ->
|
||||
@@ -1121,6 +1126,25 @@ defmodule Registry do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
|
||||
|
||||
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
|
||||
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
|
||||
else
|
||||
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
|
||||
end
|
||||
end
|
||||
|
||||
defp unregister_match_specs(_kind, key, self, pattern, guards) do
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
|
||||
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
|
||||
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers the current process under the given `key` in `registry`.
|
||||
|
||||
@@ -1180,7 +1204,14 @@ defmodule Registry do
|
||||
counter = System.unique_integer()
|
||||
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
|
||||
|
||||
case register_key(kind, key_ets, key, {key, {self, value}}) do
|
||||
key_entry =
|
||||
if ordered?(kind) do
|
||||
{{key, self, counter}, value}
|
||||
else
|
||||
{key, {self, value}}
|
||||
end
|
||||
|
||||
case register_key(kind, key_ets, key, key_entry) do
|
||||
:ok ->
|
||||
for listener <- listeners do
|
||||
Kernel.send(listener, {:register, registry, key, self, value})
|
||||
@@ -1409,21 +1440,18 @@ defmodule Registry do
|
||||
@spec count_match(registry, key, match_pattern, guards) :: non_neg_integer()
|
||||
def count_match(registry, key, pattern, guards \\ [])
|
||||
when is_atom(registry) and is_list(guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
spec = [{{:_, {:_, pattern}}, guards, [true]}]
|
||||
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select_count(key_ets, spec)
|
||||
{{:duplicate, :pid}, partitions, nil} ->
|
||||
spec = count_match_spec({:duplicate, :pid}, key, pattern, guards)
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
:ets.select_count(key_ets, spec)
|
||||
|
||||
{{:duplicate, _}, partitions, _key_ets} ->
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{kind, partitions, key_ets} ->
|
||||
spec = count_match_spec(kind, key, pattern, guards)
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select_count(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1452,6 +1480,9 @@ defmodule Registry do
|
||||
some operations like `:element` to modify the output format.
|
||||
|
||||
Do not use special match variables `:"$_"` and `:"$$"`, because they might not work as expected.
|
||||
In particular, `{:duplicate, :key}` registries use a different internal ETS layout, so match specs
|
||||
that reference the underlying entry structure via `:"$_"` will return different results.
|
||||
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
|
||||
|
||||
Note that for large registries with many partitions this will be costly as it builds the result by
|
||||
concatenating all the partitions.
|
||||
@@ -1479,15 +1510,16 @@ defmodule Registry do
|
||||
@spec select(registry, spec) :: [term]
|
||||
def select(registry, spec)
|
||||
when is_atom(registry) and is_list(spec) do
|
||||
spec = group_match_headers(spec, __ENV__.function)
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
{kind, partitions, nil} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
|
||||
Enum.flat_map(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{_kind, 1, key_ets} ->
|
||||
{kind, 1, key_ets} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
:ets.select(key_ets, spec)
|
||||
end
|
||||
end
|
||||
@@ -1510,24 +1542,30 @@ defmodule Registry do
|
||||
@spec count_select(registry, spec) :: non_neg_integer()
|
||||
def count_select(registry, spec)
|
||||
when is_atom(registry) and is_list(spec) do
|
||||
spec = group_match_headers(spec, __ENV__.function)
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
{kind, partitions, nil} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{_kind, 1, key_ets} ->
|
||||
{kind, 1, key_ets} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
:ets.select_count(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_match_headers(spec, {fun, arity}) do
|
||||
defp group_match_headers(spec, kind, {fun, arity}) do
|
||||
ordered = ordered?(kind)
|
||||
|
||||
for part <- spec do
|
||||
case part do
|
||||
{{key, pid, value}, guards, select} ->
|
||||
{{key, {pid, value}}, guards, select}
|
||||
{{key, pid, value}, guards, body} when ordered ->
|
||||
{{{key, pid, :_}, value}, guards, body}
|
||||
|
||||
{{key, pid, value}, guards, body} ->
|
||||
{{key, {pid, value}}, guards, body}
|
||||
|
||||
_ ->
|
||||
raise ArgumentError,
|
||||
@@ -1578,7 +1616,23 @@ defmodule Registry do
|
||||
:ets.lookup_element(registry, partition, 3)
|
||||
end
|
||||
|
||||
defp safe_lookup_second(ets, key) do
|
||||
defp lookup_second({:duplicate, :key}, ets, key) do
|
||||
spec =
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
|
||||
else
|
||||
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
|
||||
end
|
||||
|
||||
try do
|
||||
:ets.select(ets, spec)
|
||||
catch
|
||||
:error, :badarg -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp lookup_second(_kind, ets, key) do
|
||||
try do
|
||||
:ets.lookup_element(ets, key, 2)
|
||||
catch
|
||||
@@ -1607,7 +1661,19 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __unregister__(table, match, pos) do
|
||||
def __unregister__(table, match, pos, ordered \\ false)
|
||||
|
||||
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
|
||||
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
|
||||
else
|
||||
:ets.match_delete(table, {{key, pid, :_}, :_})
|
||||
end
|
||||
end
|
||||
|
||||
def __unregister__(table, match, pos, false = _ordered) do
|
||||
key = :erlang.element(pos, match)
|
||||
|
||||
# We need to perform an element comparison if we have a special atom key.
|
||||
@@ -1620,9 +1686,52 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
# For {:duplicate, :key} registries, the ETS entry layout is
|
||||
# {{key, pid, counter}, value} (ordered_set with composite key),
|
||||
# so :"$_" refers to that whole tuple. The body extracts {pid, value}
|
||||
# to match the public API's return shape of [{pid, value}].
|
||||
defp match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
|
||||
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
guards = [
|
||||
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
|
||||
]
|
||||
|
||||
[{{{:_, :_, :_}, pattern}, guards, body}]
|
||||
else
|
||||
[{{{key, :_, :_}, pattern}, guards, body}]
|
||||
end
|
||||
end
|
||||
|
||||
defp match_spec(_kind, key, pattern, guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
end
|
||||
|
||||
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
guards = [
|
||||
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
|
||||
]
|
||||
|
||||
[{{{:_, :_, :_}, pattern}, guards, [true]}]
|
||||
else
|
||||
[{{{key, :_, :_}, pattern}, guards, [true]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp count_match_spec(_kind, key, pattern, guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [true]}]
|
||||
end
|
||||
|
||||
defp reserved_atom?("_"), do: true
|
||||
defp reserved_atom?("$" <> _), do: true
|
||||
defp reserved_atom?(_), do: false
|
||||
|
||||
defp ordered?({:duplicate, :key}), do: true
|
||||
defp ordered?(_), do: false
|
||||
end
|
||||
|
||||
defmodule Registry.Supervisor do
|
||||
@@ -1734,7 +1843,8 @@ defmodule Registry.Partition do
|
||||
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
|
||||
end
|
||||
|
||||
{:ok, {pid_ets, %{}}}
|
||||
ordered = kind == {:duplicate, :key}
|
||||
{:ok, {pid_ets, %{}, ordered}}
|
||||
end
|
||||
|
||||
# The key partition is a set for unique keys,
|
||||
@@ -1744,7 +1854,12 @@ defmodule Registry.Partition do
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
|
||||
defp init_key_ets({:duplicate, :key}, key_partition, compressed) do
|
||||
opts = [:ordered_set, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
defp init_key_ets({:duplicate, :pid}, key_partition, compressed) do
|
||||
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
@@ -1768,7 +1883,7 @@ defmodule Registry.Partition do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:lock, key}, from, {ets, lock}) do
|
||||
def handle_call({:lock, key}, from, {ets, lock, ordered}) do
|
||||
lock =
|
||||
case lock do
|
||||
%{^key => queue} ->
|
||||
@@ -1779,10 +1894,10 @@ defmodule Registry.Partition do
|
||||
Map.put(lock, key, :queue.new())
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do
|
||||
entries = :ets.take(ets, pid)
|
||||
|
||||
for {_pid, key, key_ets, _counter} <- entries do
|
||||
@@ -1797,13 +1912,13 @@ defmodule Registry.Partition do
|
||||
end
|
||||
|
||||
try do
|
||||
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1)
|
||||
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered)
|
||||
catch
|
||||
:error, :badarg -> :badarg
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
end
|
||||
|
||||
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
|
||||
@@ -1815,7 +1930,7 @@ defmodule Registry.Partition do
|
||||
unlock(key, state)
|
||||
end
|
||||
|
||||
defp unlock(key, {ets, lock}) do
|
||||
defp unlock(key, {ets, lock, ordered}) do
|
||||
%{^key => queue} = lock
|
||||
|
||||
lock =
|
||||
@@ -1824,7 +1939,7 @@ defmodule Registry.Partition do
|
||||
{:not_empty, queue} -> Map.put(lock, key, queue)
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
end
|
||||
|
||||
defp dequeue(queue, key) do
|
||||
|
||||
@@ -1437,7 +1437,7 @@ defmodule Stream do
|
||||
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
|
||||
|
||||
{_, []} ->
|
||||
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
|
||||
do_cycle(check_cycle_subsequent_element(cycle), [], cycle, {:cont, acc}, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1446,10 +1446,26 @@ defmodule Stream do
|
||||
end
|
||||
|
||||
defp check_cycle_first_element(reduce) do
|
||||
check_cycle_non_empty(
|
||||
reduce,
|
||||
ArgumentError,
|
||||
"cannot cycle over an empty enumerable"
|
||||
)
|
||||
end
|
||||
|
||||
defp check_cycle_subsequent_element(reduce) do
|
||||
check_cycle_non_empty(
|
||||
reduce,
|
||||
RuntimeError,
|
||||
"cycled enumerable became empty after a previous iteration produced elements"
|
||||
)
|
||||
end
|
||||
|
||||
defp check_cycle_non_empty(reduce, exception, message) do
|
||||
fn acc ->
|
||||
case reduce.(acc) do
|
||||
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
|
||||
raise ArgumentError, "cannot cycle over an empty enumerable"
|
||||
raise exception, message
|
||||
|
||||
other ->
|
||||
other
|
||||
|
||||
@@ -745,7 +745,15 @@ defmodule String do
|
||||
iex> String.count("hello world", "")
|
||||
12
|
||||
|
||||
The `pattern` can also be a compiled pattern:
|
||||
The `pattern` can also be a list:
|
||||
|
||||
iex> String.count("hello world", ["e", "o"])
|
||||
3
|
||||
|
||||
iex> String.count("hello world", [])
|
||||
0
|
||||
|
||||
Or a compiled pattern:
|
||||
|
||||
iex> pattern = :binary.compile_pattern([" ", "!"])
|
||||
iex> String.count("foo bar baz!!", pattern)
|
||||
@@ -756,6 +764,8 @@ defmodule String do
|
||||
@doc since: "1.19.0"
|
||||
def count(string, <<>>), do: length(string) + 1
|
||||
|
||||
def count(_string, []), do: 0
|
||||
|
||||
def count(string, pattern) when is_struct(pattern, Regex) do
|
||||
Kernel.length(Regex.scan(pattern, string, return: :index))
|
||||
end
|
||||
@@ -2290,6 +2300,19 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
# 56-bit SWAR guard: all 7 bytes are ASCII (< 128) and none is \r (0x0D).
|
||||
# Uses Mycroft's zero-byte detection: XOR with 0x0D and check for zero bytes.
|
||||
defguardp ascii_no_cr_swar?(w)
|
||||
when Bitwise.band(w, 0x80808080808080) == 0 and
|
||||
Bitwise.band(
|
||||
Bitwise.bxor(w, 0x0D0D0D0D0D0D0D) - 0x01010101010101,
|
||||
0x80808080808080
|
||||
) == 0
|
||||
|
||||
defp skip_length(<<w::56, b, rest::binary>>, acc)
|
||||
when b <= 127 and b != ?\r and ascii_no_cr_swar?(w),
|
||||
do: skip_length(rest, acc + 8)
|
||||
|
||||
defp skip_length(<<byte, rest::binary>>, acc)
|
||||
when byte <= 127 and byte != ?\r,
|
||||
do: skip_length(rest, acc + 1)
|
||||
@@ -3213,7 +3236,13 @@ defmodule String do
|
||||
byte_size_unicode(unicode)
|
||||
end
|
||||
|
||||
defp byte_size_remaining_at(unicode, n) do
|
||||
defp byte_size_remaining_at(<<byte1, byte2, rest::binary>> = binary, n)
|
||||
when n > 0 and byte1 <= 127 and byte1 != ?\r and byte2 <= 127 and byte2 != ?\r do
|
||||
skip = min(skip_length(rest, 1), n)
|
||||
byte_size_remaining_at(binary_part(binary, skip, byte_size(binary) - skip), n - skip)
|
||||
end
|
||||
|
||||
defp byte_size_remaining_at(unicode, n) when n > 0 do
|
||||
case :unicode_util.gc(unicode) do
|
||||
[_] -> 0
|
||||
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
|
||||
|
||||
@@ -556,19 +556,29 @@ defmodule Supervisor do
|
||||
|
||||
Developers typically invoke `Supervisor.init/2` at the end of their
|
||||
init callback to return the proper supervision flags.
|
||||
|
||||
See `start_link/2` for more information.
|
||||
"""
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok,
|
||||
{sup_flags(), [child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
|
||||
| :ignore
|
||||
|
||||
@typedoc "Return values of `start_link/2` and `start_link/3`."
|
||||
@typedoc """
|
||||
Return values of `start_link/2` and `start_link/3`.
|
||||
|
||||
See the documentation for those functions for more information.
|
||||
"""
|
||||
@type on_start ::
|
||||
{:ok, pid}
|
||||
| :ignore
|
||||
| {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
|
||||
@typedoc "Return values of `start_child/2`."
|
||||
@typedoc """
|
||||
Return values of `start_child/2`.
|
||||
|
||||
See the documentation for those functions for more information.
|
||||
"""
|
||||
@type on_start_child ::
|
||||
{:ok, child}
|
||||
| {:ok, child, info :: term}
|
||||
@@ -698,15 +708,18 @@ defmodule Supervisor do
|
||||
If the supervisor and all child processes are successfully spawned
|
||||
(if the start function of each child process returns `{:ok, child}`,
|
||||
`{:ok, child, info}`, or `:ignore`), this function returns
|
||||
`{:ok, pid}`, where `pid` is the PID of the supervisor. If the supervisor
|
||||
is given a name and a process with the specified name already exists,
|
||||
the function returns `{:error, {:already_started, pid}}`, where `pid`
|
||||
is the PID of that process.
|
||||
`{:ok, pid}`, where `pid` is the PID of the supervisor.
|
||||
|
||||
If the start function of any of the child processes fails or returns an error
|
||||
tuple or an erroneous value, the supervisor first terminates with reason
|
||||
`:shutdown` all the child processes that have already been started, and then
|
||||
terminates itself and returns `{:error, {:shutdown, reason}}`.
|
||||
If the supervisor is given a name and a process with the specified name
|
||||
already exists, the function returns `{:error, {:already_started, pid}}`,
|
||||
where `pid` is the PID of that process.
|
||||
|
||||
If the start function of any of the child processes fails or returns
|
||||
an error tuple or an erroneous value, the supervisor first terminates
|
||||
with reason `:shutdown` all the child processes that have already been
|
||||
started, and then terminates itself. If the caller process is trapping
|
||||
exits, this function then returns
|
||||
`{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
|
||||
|
||||
Note that a supervisor started with this function is linked to the parent
|
||||
process and exits not only on crashes but also if the parent process exits
|
||||
@@ -939,9 +952,17 @@ defmodule Supervisor do
|
||||
|
||||
If the `c:init/1` callback returns `:ignore`, this function returns
|
||||
`:ignore` as well and the supervisor terminates with reason `:normal`.
|
||||
If it fails or returns an incorrect value, this function returns
|
||||
`{:error, term}` where `term` is a term with information about the
|
||||
error, and the supervisor terminates with reason `term`.
|
||||
|
||||
If the `c:init/1` callback fails or returns an incorrect value, the supervisor
|
||||
will shutdown with reason `term`, where `term` contains information
|
||||
about the error. If the caller process is trapping exits, this function then
|
||||
returns `{:error, term()}`.
|
||||
|
||||
If the start function of any of the child processes returned by `c:init/1`
|
||||
fail or return an error tuple or an erroneous value, the supervisor first
|
||||
terminates with reason `:shutdown` all the child processes that have already
|
||||
been started, and then terminates itself. If the caller process is trapping
|
||||
exits, this function then returns `{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
|
||||
|
||||
The `:name` option can also be given in order to register a supervisor
|
||||
name, the supported values are described in the "Name registration"
|
||||
|
||||
@@ -75,7 +75,7 @@ iex> trunc(3.58)
|
||||
3
|
||||
```
|
||||
|
||||
Finally, we work with different data types, we will learn Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
|
||||
Finally, as we work with different data types, we will learn that Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
|
||||
|
||||
```elixir
|
||||
iex> is_integer(1)
|
||||
@@ -310,4 +310,4 @@ false
|
||||
|
||||
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`).
|
||||
|
||||
Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward.
|
||||
Elixir also provides data-types for expressing collections, such as lists and tuples, which we'll learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come in later chapters. Let's move forward.
|
||||
|
||||
@@ -241,7 +241,7 @@ $ mix help
|
||||
mix deps # Lists dependencies and their status
|
||||
mix deps.clean # Deletes the given dependencies' files
|
||||
mix deps.compile # Compiles dependencies
|
||||
mix deps.get # Gets all out of date dependencies
|
||||
mix deps.get # Fetches unavailable and out of date dependencies
|
||||
mix deps.tree # Prints the dependency tree
|
||||
mix deps.unlock # Unlocks the given dependencies
|
||||
mix deps.update # Updates the given dependencies
|
||||
|
||||
@@ -143,7 +143,7 @@ With this change, we are saying that we want to run `KV.Server.accept(4040)` as
|
||||
Now that the server is part of the supervision tree, it should start automatically when we run the application. Run `iex -S mix` to boot the app and use the `telnet` client to make sure that everything still works:
|
||||
|
||||
```console
|
||||
$ telnet 127.0.0.1 4321
|
||||
$ telnet 127.0.0.1 4040
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
@@ -158,7 +158,7 @@ Yes, it works! However, can it handle more than one client?
|
||||
Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo:
|
||||
|
||||
```console
|
||||
$ telnet 127.0.0.1 4321
|
||||
$ telnet 127.0.0.1 4040
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
|
||||
@@ -233,13 +233,13 @@ Inferring type signatures comes with a series of trade-offs:
|
||||
|
||||
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
|
||||
|
||||
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
|
||||
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
|
||||
|
||||
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
|
||||
In the long term, Elixir developers who want static typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
|
||||
|
||||
## Roadmap
|
||||
|
||||
The current milestone is to implement type inference of existing codebases, as well as type checking of all language constructs, without changes to the Elixir language. At this stage, we want to collect feedback on the quality of error messages and performance, and therefore the type system has no user facing API. Full type inference of patterns was released in Elixir v1.18, and complete inference is expected as part of Elixir v1.20.
|
||||
At this moment, Elixir implements type inference of all language constructs. The goal is to assess performance and collect feedback on the quality of error messages, before introducing user facing types.
|
||||
|
||||
If the results are satisfactory, the next milestone will include a mechanism for defining typed structs. Elixir programs frequently pattern match on structs, which reveals information about the struct fields, but it knows nothing about their respective types. By propagating types from structs and their fields throughout the program, we will increase the type system’s ability to find errors while further straining our type system implementation. Proposals including the required changes to the language surface will be sent to the community once we reach this stage.
|
||||
|
||||
|
||||
+26
-13
@@ -304,6 +304,9 @@ eval_forms(Tree, Binding, OrigE) ->
|
||||
eval_forms(Tree, Binding, OrigE, []).
|
||||
eval_forms(Tree, Binding, OrigE, Opts) ->
|
||||
Prune = proplists:get_value(prune_binding, Opts, false),
|
||||
%% We save and restore the previous dbg_callback so nested eval calls in
|
||||
%% the same process do not clobber the outer callback.
|
||||
PreviousDbg = erlang:get({elixir, dbg_callback}),
|
||||
case proplists:get_value(dbg_callback, Opts) of
|
||||
undefined -> ok;
|
||||
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
|
||||
@@ -332,7 +335,10 @@ eval_forms(Tree, Binding, OrigE, Opts) ->
|
||||
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
|
||||
end
|
||||
after
|
||||
erlang:erase({elixir, dbg_callback})
|
||||
case PreviousDbg of
|
||||
undefined -> erlang:erase({elixir, dbg_callback});
|
||||
_ -> erlang:put({elixir, dbg_callback}, PreviousDbg)
|
||||
end
|
||||
end.
|
||||
|
||||
%% Evaluate Erlang code with careful handling of local and external functions
|
||||
@@ -341,20 +347,27 @@ erl_eval(Expr, Binding, Env) ->
|
||||
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
|
||||
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
|
||||
|
||||
%% ?elixir_eval_env is used by the external handler.
|
||||
%%
|
||||
%% The reason why we use the process dictionary to pass the environment
|
||||
%% is because we want to avoid passing closures to erl_eval, as that
|
||||
%% would effectively tie the eval code to the Elixir version and it is
|
||||
%% best if it depends solely on Erlang/OTP.
|
||||
%%
|
||||
%% The downside is that functions that escape the eval context will no
|
||||
%% longer have the original environment they came from.
|
||||
%%
|
||||
%% We save and restore the previous env so nested eval calls in the same
|
||||
%% process do not clobber the outer env.
|
||||
PreviousEvalEnv = erlang:get(?elixir_eval_env),
|
||||
erlang:put(?elixir_eval_env, Env),
|
||||
try
|
||||
%% ?elixir_eval_env is used by the external handler.
|
||||
%%
|
||||
%% The reason why we use the process dictionary to pass the environment
|
||||
%% is because we want to avoid passing closures to erl_eval, as that
|
||||
%% would effectively tie the eval code to the Elixir version and it is
|
||||
%% best if it depends solely on Erlang/OTP.
|
||||
%%
|
||||
%% The downside is that functions that escape the eval context will no
|
||||
%% longer have the original environment they came from.
|
||||
erlang:put(?elixir_eval_env, Env),
|
||||
erl_eval:expr(Expr, Binding, LocalHandler, ExternalHandler)
|
||||
after
|
||||
erlang:erase(?elixir_eval_env)
|
||||
case PreviousEvalEnv of
|
||||
undefined -> erlang:erase(?elixir_eval_env);
|
||||
_ -> erlang:put(?elixir_eval_env, PreviousEvalEnv)
|
||||
end
|
||||
end.
|
||||
|
||||
eval_local_handler(FunName, Args) ->
|
||||
@@ -439,7 +452,7 @@ quoted_to_erl(Quoted, ErlS, ExS, Env) ->
|
||||
|
||||
%% Converts a given string (charlist) into quote expression
|
||||
|
||||
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
|
||||
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_integer(StartColumn), is_binary(File) ->
|
||||
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, Opts) of
|
||||
{ok, _Line, _Column, Warnings, Tokens, Terminators} ->
|
||||
{ok, lists:reverse(Tokens, Terminators), Warnings};
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
prematch=none,
|
||||
%% Stores if __STACKTRACE__ is allowed
|
||||
stacktrace=false,
|
||||
%% A map of unused vars and a version counter for vars
|
||||
unused={#{}, 0},
|
||||
%% A map of unused vars
|
||||
unused=#{},
|
||||
%% A list of modules defined in functions (runtime)
|
||||
runtime_modules=[],
|
||||
%% A tuple with maps of read and optional write current vars.
|
||||
@@ -46,7 +46,9 @@
|
||||
%% if you write foo(a = 123), the value of `a` cannot be
|
||||
%% read in the following argument, only after the call
|
||||
%%
|
||||
vars={#{}, false}
|
||||
vars={#{}, false},
|
||||
%% Stores expression version counter
|
||||
version=0
|
||||
}).
|
||||
|
||||
-record(elixir_erl, {
|
||||
|
||||
@@ -61,7 +61,7 @@ expand(BitstrMeta, Fun, [H | T], Acc, S, E, Alignment, RequireSize) ->
|
||||
|
||||
InferredMeta = [{inferred_bitstring_spec, true} | Meta],
|
||||
EAcc = concat_or_prepend_bitstring(InferredMeta, ELeft, ERight, Acc, ES, MatchOrRequireSize),
|
||||
expand(Meta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
|
||||
expand(BitstrMeta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
|
||||
|
||||
extract_meta({_, Meta, _}, _) -> Meta;
|
||||
extract_meta(_, Meta) -> Meta.
|
||||
@@ -115,7 +115,9 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac
|
||||
[{'::', Meta, [ELeft, ERight]} | Acc]
|
||||
end;
|
||||
{bitstring, _, nil} ->
|
||||
lists:reverse(Parts, Acc)
|
||||
lists:reverse(Parts, Acc);
|
||||
_ ->
|
||||
[{'::', Meta, [ELeft, ERight]} | Acc]
|
||||
end;
|
||||
concat_or_prepend_bitstring(Meta, ELeft, ERight, Acc, _E, _RequireSize) ->
|
||||
[{'::', Meta, [ELeft, ERight]} | Acc].
|
||||
|
||||
@@ -155,13 +155,14 @@ recur_cycles(Cycles, Current, Source, Seen, SkipList, Meta, Expr, E) ->
|
||||
%% Match
|
||||
|
||||
match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
|
||||
#elixir_ex{vars=Current, unused={_, Counter} = Unused} = AfterS,
|
||||
#elixir_ex{vars=Current, unused=Unused, version=Counter} = AfterS,
|
||||
#elixir_ex{vars={Read, _}, prematch=Prematch} = BeforeS,
|
||||
|
||||
CallS = BeforeS#elixir_ex{
|
||||
prematch={Read, {#{}, []}, Counter},
|
||||
unused=Unused,
|
||||
vars=Current
|
||||
vars=Current,
|
||||
version=Counter
|
||||
},
|
||||
|
||||
CallE = E#{context := match},
|
||||
@@ -170,7 +171,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
|
||||
#elixir_ex{
|
||||
vars=NewCurrent,
|
||||
unused=NewUnused,
|
||||
prematch={_, Cycles, _}
|
||||
prematch={_, Cycles, _},
|
||||
version=NewCounter
|
||||
} = SE,
|
||||
|
||||
validate_cycles(Cycles, Meta, {match, Expr}, E),
|
||||
@@ -178,7 +180,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
|
||||
EndS = AfterS#elixir_ex{
|
||||
prematch=Prematch,
|
||||
unused=NewUnused,
|
||||
vars=NewCurrent
|
||||
vars=NewCurrent,
|
||||
version=NewCounter
|
||||
},
|
||||
|
||||
EndE = EE#{context := ?key(E, context)},
|
||||
@@ -319,7 +322,9 @@ with(Meta, Args, S, E) ->
|
||||
ok
|
||||
end,
|
||||
|
||||
{{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, S3, E}.
|
||||
#elixir_ex{version=Counter} = S3,
|
||||
{{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]},
|
||||
S3#elixir_ex{version=Counter+1}, E}.
|
||||
|
||||
expand_with({'<-', Meta, [Left, Right]}, {S, E, HasMatch}) ->
|
||||
{ERight, SR, ER} = elixir_expand:expand(Right, S, E),
|
||||
@@ -535,8 +540,8 @@ warn_catch_before_rescue([_ | Rest], Meta, E, Found) ->
|
||||
warn_catch_before_rescue(Rest, Meta, E, Found).
|
||||
|
||||
origin(Meta, Default) ->
|
||||
case lists:keyfind(origin, 1, Meta) of
|
||||
{origin, Origin} -> Origin;
|
||||
case lists:keyfind(definition, 1, Meta) of
|
||||
{definition, Definition} -> Definition;
|
||||
false -> Default
|
||||
end.
|
||||
|
||||
|
||||
@@ -98,9 +98,11 @@ do_is_purgeable(<<_:4/binary, Size:32, Beam/binary>>) ->
|
||||
do_is_purgeable(Rest).
|
||||
|
||||
dispatch(Module, Fun, Args, Purgeable) ->
|
||||
Res = Module:Fun(Args),
|
||||
return_compiler_module(Module, Purgeable),
|
||||
Res.
|
||||
try
|
||||
Module:Fun(Args)
|
||||
after
|
||||
return_compiler_module(Module, Purgeable)
|
||||
end.
|
||||
|
||||
code_fun(nil) -> '__FILE__';
|
||||
code_fun(_) -> '__MODULE__'.
|
||||
|
||||
@@ -247,7 +247,7 @@ def_to_clauses(_Kind, Meta, Args, Guards, [{do, Body}], _E) ->
|
||||
def_to_clauses(Kind, Meta, Args, Guards, Body, E) ->
|
||||
case is_list(Body) andalso lists:keyfind(do, 1, Body) of
|
||||
{do, _} ->
|
||||
[{Meta, Args, Guards, {'try', [{origin, Kind} | Meta], [Body]}}];
|
||||
[{Meta, Args, Guards, {'try', [{definition, Kind} | Meta], [Body]}}];
|
||||
false ->
|
||||
elixir_errors:file_error(Meta, E, elixir_expand, {missing_option, Kind, [do]})
|
||||
end.
|
||||
@@ -295,17 +295,17 @@ store_definition(CheckClauses, Kind, Meta, Name, Arity, File, Module, Defaults,
|
||||
%% Handling of defaults
|
||||
|
||||
unpack_defaults(Kind, Meta, Name, Args, S, E) ->
|
||||
{Expanded, #elixir_ex{unused={_, VersionOffset}}} = expand_defaults(Args, S, E#{context := nil}, []),
|
||||
unpack_expanded(Kind, Meta, Name, Expanded, VersionOffset, [], []).
|
||||
{Expanded, #elixir_ex{version=Counter}} = expand_defaults(Args, S, E#{context := nil}, []),
|
||||
unpack_expanded(Kind, Meta, Name, Expanded, Counter, [], []).
|
||||
|
||||
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) ->
|
||||
Base = match_defaults(Acc, length(Acc) + VersionOffset, []),
|
||||
{Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []),
|
||||
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, Counter, Acc, Clauses) ->
|
||||
Base = match_defaults(Acc, length(Acc) + Counter, []),
|
||||
{Args, Invoke} = extract_defaults(List, length(Base) + Counter, [], []),
|
||||
Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}},
|
||||
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]);
|
||||
unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) ->
|
||||
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses);
|
||||
unpack_expanded(_Kind, _Meta, _Name, [], _VersionOffset, Acc, Clauses) ->
|
||||
unpack_expanded(Kind, Meta, Name, T, Counter, [Expr | Acc], [Clause | Clauses]);
|
||||
unpack_expanded(Kind, Meta, Name, [H | T], Counter, Acc, Clauses) ->
|
||||
unpack_expanded(Kind, Meta, Name, T, Counter, [H | Acc], Clauses);
|
||||
unpack_expanded(_Kind, _Meta, _Name, [], _Counter, Acc, Clauses) ->
|
||||
{lists:reverse(Acc), lists:reverse(Clauses)}.
|
||||
|
||||
expand_defaults([{'\\\\', Meta, [Expr, Default]} | Args], S, E, Acc) ->
|
||||
|
||||
@@ -349,7 +349,7 @@ prune_stacktrace([{_, _, [Caller | _], _} | _], _MFA, Info, {ok, Caller}) ->
|
||||
prune_stacktrace([{M, F, A, _} | _], {M, F, A}, Info, _E) ->
|
||||
Info;
|
||||
%% We've reached the elixir_dispatch internals, skip it with the rest
|
||||
prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_exp ->
|
||||
prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_expand ->
|
||||
Info;
|
||||
prune_stacktrace([H | T], MFA, Info, E) ->
|
||||
[H | prune_stacktrace(T, MFA, Info, E)];
|
||||
@@ -363,7 +363,7 @@ format_error({import, {conflict, Receiver}, Name, Arity}) ->
|
||||
"please rename the local macro or remove the conflicting import",
|
||||
[Name, Arity, elixir_aliases:inspect(Receiver), Name, Arity]);
|
||||
format_error({import, {ambiguous, [Mod1, Mod2 | _]}, Name, Arity}) ->
|
||||
io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous",
|
||||
io_lib:format("conflicting ~ts/~B import from modules ~ts and ~ts",
|
||||
[Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]);
|
||||
format_error({compile_env, Name, Arity}) ->
|
||||
io_lib:format("Application.~s/~B is discouraged in the module body, use Application.compile_env/3 instead", [Name, Arity]);
|
||||
|
||||
@@ -56,12 +56,12 @@ env_to_ex(#{context := match, versioned_vars := Vars}) ->
|
||||
#elixir_ex{
|
||||
prematch={Vars, {#{}, []}, Counter},
|
||||
vars={Vars, false},
|
||||
unused={#{}, Counter}
|
||||
version=Counter
|
||||
};
|
||||
env_to_ex(#{versioned_vars := Vars}) ->
|
||||
#elixir_ex{
|
||||
vars={Vars, false},
|
||||
unused={#{}, map_size(Vars)}
|
||||
version=map_size(Vars)
|
||||
}.
|
||||
|
||||
%% VAR HANDLING
|
||||
@@ -94,10 +94,10 @@ merge_vars(V1, V2) ->
|
||||
|
||||
%% UNUSED VARS
|
||||
|
||||
reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
|
||||
S#elixir_ex{unused={#{}, Version}}.
|
||||
reset_unused_vars(#elixir_ex{} = S) ->
|
||||
S#elixir_ex{unused=#{}}.
|
||||
|
||||
check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
|
||||
check_unused_vars(#elixir_ex{unused=Unused}, E) ->
|
||||
[elixir_errors:file_warn(calculate_span(Meta, Name), E, ?MODULE, {unused_var, Name, Overridden}) ||
|
||||
{{{Name, _Kind}, _Count}, {Meta, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
|
||||
E.
|
||||
@@ -111,10 +111,10 @@ calculate_span(Meta, Name) ->
|
||||
Meta
|
||||
end.
|
||||
|
||||
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) ->
|
||||
#elixir_ex{unused={ClauseUnused, Version}} = S,
|
||||
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused=Unused}, E) ->
|
||||
#elixir_ex{unused=ClauseUnused} = S,
|
||||
NewUnused = merge_and_check_unused_vars(Read, Unused, ClauseUnused, E),
|
||||
S#elixir_ex{unused={NewUnused, Version}, vars={Read, Write}}.
|
||||
S#elixir_ex{unused=NewUnused, vars={Read, Write}}.
|
||||
|
||||
merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
|
||||
maps:fold(fun
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
-define(typespecs, 'Elixir.Kernel.Typespec').
|
||||
|
||||
checker_version() ->
|
||||
elixir_checker_v7.
|
||||
elixir_checker_v8.
|
||||
|
||||
%% debug_info callback
|
||||
|
||||
|
||||
@@ -318,8 +318,10 @@ no_var(ParentAnn, Elements) ->
|
||||
|
||||
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
|
||||
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
|
||||
no_var_size({var, _, _}) -> throw(unbound_size);
|
||||
no_var_size(Size) -> Size.
|
||||
|
||||
no_var_size(default) -> default;
|
||||
no_var_size(Size) when is_integer(Size) -> Size;
|
||||
no_var_size(_) -> throw(unbound_size).
|
||||
|
||||
build_comprehension(Ann, Clauses, Expr, Into) ->
|
||||
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
|
||||
|
||||
@@ -576,19 +576,6 @@ translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
|
||||
{clause, Generated, [Var], [[Guard]], [Fast]},
|
||||
{clause, Generated, [Var], [], [Slow]}
|
||||
]}, VS};
|
||||
translate_remote(maps, put, Meta, [Key, Value, Map], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
case translate_args([Key, Value, Map], Ann, S) of
|
||||
{[TKey, TValue, {map, _, InnerMap, Pairs}], TS} ->
|
||||
{{map, Ann, InnerMap, Pairs ++ [{map_field_assoc, Ann, TKey, TValue}]}, TS};
|
||||
|
||||
{[TKey, TValue, {map, _, Pairs}], TS} ->
|
||||
{{map, Ann, Pairs ++ [{map_field_assoc, Ann, TKey, TValue}]}, TS};
|
||||
|
||||
{[TKey, TValue, TMap], TS} ->
|
||||
{{map, Ann, TMap, [{map_field_assoc, Ann, TKey, TValue}]}, TS}
|
||||
end;
|
||||
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ rescue_guards(Meta, Var, Aliases) ->
|
||||
ElixirGuards =
|
||||
[erl_and(Meta,
|
||||
{erl(Meta, '=='), Meta, [{erl(Meta, map_get), Meta, ['__struct__', Var]}, Alias]},
|
||||
{erl(Meta, map_get), Meta, ['__exception__', Var]}
|
||||
{erl(Meta, is_map_key), Meta, ['__exception__', Var]}
|
||||
) || Alias <- Aliases],
|
||||
|
||||
{ElixirGuards ++ ErlangGuards, ErlangAliases}.
|
||||
@@ -214,7 +214,11 @@ erl_rescue_guard_for(Meta, Var, 'Elixir.ErlangError') ->
|
||||
erl_and(
|
||||
Meta,
|
||||
{erl(Meta, is_map), Meta, [Var]},
|
||||
{erl(Meta, is_map_key), Meta, ['__exception__', Var]}
|
||||
erl_and(
|
||||
Meta,
|
||||
{erl(Meta, is_map_key), Meta, ['__struct__', Var]},
|
||||
{erl(Meta, is_map_key), Meta, ['__exception__', Var]}
|
||||
)
|
||||
),
|
||||
{erl(Meta, 'not'), Meta, [Condition]};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ build(Key, #elixir_erl{counter=Counter} = S) ->
|
||||
{build_name(Key, Count),
|
||||
S#elixir_erl{counter=Counter#{Key => Count}}}.
|
||||
|
||||
build_name('_', Count) -> list_to_atom("_@" ++ integer_to_list(Count));
|
||||
build_name('_', Count) -> list_to_atom("_" ++ integer_to_list(Count));
|
||||
build_name(Name, Count) -> list_to_atom("_" ++ atom_to_list(Name) ++ "@" ++ integer_to_list(Count)).
|
||||
|
||||
%% BINDINGS
|
||||
@@ -102,7 +102,7 @@ load_pair({Pair, Value}) -> {Pair, Value}.
|
||||
|
||||
dump_binding(Binding, ErlS, ExS, PruneBefore) ->
|
||||
#elixir_erl{var_names=ErlVars} = ErlS,
|
||||
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
|
||||
#elixir_ex{vars={ExVars, _}, unused=Unused} = ExS,
|
||||
|
||||
maps:fold(fun
|
||||
({Var, Kind} = Pair, Version, {B, V})
|
||||
|
||||
@@ -331,7 +331,8 @@ expand({'cond', Meta, [Opts]}, S, E) ->
|
||||
assert_no_match_or_guard_scope(Meta, "cond", S, E),
|
||||
assert_no_underscore_clause_in_cond(Opts, E),
|
||||
{EClauses, SC, EC} = elixir_clauses:'cond'(Meta, Opts, S, E),
|
||||
{{'cond', Meta, [EClauses]}, SC, EC};
|
||||
#elixir_ex{version=Counter} = SC,
|
||||
{{'cond', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC};
|
||||
|
||||
expand({'case', Meta, [Expr, Options]}, S, E) ->
|
||||
assert_no_match_or_guard_scope(Meta, "case", S, E),
|
||||
@@ -340,12 +341,14 @@ expand({'case', Meta, [Expr, Options]}, S, E) ->
|
||||
expand({'receive', Meta, [Opts]}, S, E) ->
|
||||
assert_no_match_or_guard_scope(Meta, "receive", S, E),
|
||||
{EClauses, SC, EC} = elixir_clauses:'receive'(Meta, Opts, S, E),
|
||||
{{'receive', Meta, [EClauses]}, SC, EC};
|
||||
#elixir_ex{version=Counter} = SC,
|
||||
{{'receive', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC};
|
||||
|
||||
expand({'try', Meta, [Opts]}, S, E) ->
|
||||
assert_no_match_or_guard_scope(Meta, "try", S, E),
|
||||
{EClauses, SC, EC} = elixir_clauses:'try'(Meta, Opts, S, E),
|
||||
{{'try', Meta, [EClauses]}, SC, EC};
|
||||
#elixir_ex{version=Counter} = SC,
|
||||
{{'try', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC};
|
||||
|
||||
%% Comprehensions
|
||||
|
||||
@@ -385,20 +388,22 @@ expand({'^', Meta, [Arg]}, S, E) ->
|
||||
function_error(Meta, E, ?MODULE, {pin_outside_of_match, Arg}),
|
||||
{{'^', Meta, [Arg]}, S#elixir_ex{tainted_function=true}, E};
|
||||
|
||||
expand({'_', Meta, Kind} = Var, S, #{context := Context} = E) when is_atom(Kind) ->
|
||||
expand({'_', Meta, Kind}, #elixir_ex{version=Counter} = S, #{context := Context} = E) when is_atom(Kind) ->
|
||||
NewVar = {'_', [{version, Counter} | Meta], Kind},
|
||||
|
||||
case Context of
|
||||
match ->
|
||||
{Var, S, E};
|
||||
{NewVar, S#elixir_ex{version=Counter+1}, E};
|
||||
_ ->
|
||||
function_error(Meta, E, ?MODULE, unbound_underscore),
|
||||
{Var, S#elixir_ex{tainted_function=true}, E}
|
||||
{NewVar, S#elixir_ex{tainted_function=true, version=Counter+1}, E}
|
||||
end;
|
||||
|
||||
expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_atom(Kind) ->
|
||||
#elixir_ex{
|
||||
prematch={_, _, PrematchVersion},
|
||||
unused={Unused, Version},
|
||||
vars={Read, Write}
|
||||
vars={Read, Write},
|
||||
unused=Unused
|
||||
} = S,
|
||||
|
||||
Pair = {Name, elixir_utils:var_context(Meta, Kind)},
|
||||
@@ -408,29 +413,31 @@ expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_at
|
||||
#{Pair := VarVersion} when VarVersion >= PrematchVersion ->
|
||||
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E),
|
||||
NewUnused = var_used(Pair, Meta, VarVersion, Unused),
|
||||
NewWrite = (Write /= false) andalso Write#{Pair => Version},
|
||||
NewWrite = (Write /= false) andalso Write#{Pair => VarVersion},
|
||||
Var = {Name, [{version, VarVersion} | Meta], Kind},
|
||||
{Var, S#elixir_ex{vars={Read, NewWrite}, unused={NewUnused, Version}}, E};
|
||||
{Var, S#elixir_ex{vars={Read, NewWrite}, unused=NewUnused}, E};
|
||||
|
||||
%% Variable is being overridden now
|
||||
#{Pair := _} ->
|
||||
Version = S#elixir_ex.version,
|
||||
NewUnused = var_unused(Pair, Meta, Version, Unused, true),
|
||||
NewRead = Read#{Pair => Version},
|
||||
NewWrite = (Write /= false) andalso Write#{Pair => Version},
|
||||
Var = {Name, [{version, Version} | Meta], Kind},
|
||||
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused={NewUnused, Version + 1}}, E};
|
||||
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused=NewUnused, version=Version + 1}, E};
|
||||
|
||||
%% Variable defined for the first time
|
||||
_ ->
|
||||
Version = S#elixir_ex.version,
|
||||
NewUnused = var_unused(Pair, Meta, Version, Unused, false),
|
||||
NewRead = Read#{Pair => Version},
|
||||
NewWrite = (Write /= false) andalso Write#{Pair => Version},
|
||||
Var = {Name, [{version, Version} | Meta], Kind},
|
||||
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused={NewUnused, Version + 1}}, E}
|
||||
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused=NewUnused, version=Version + 1}, E}
|
||||
end;
|
||||
|
||||
expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
|
||||
#elixir_ex{vars={Read, _Write}, unused={Unused, Version}, prematch=Prematch} = S,
|
||||
#elixir_ex{vars={Read, _Write}, unused=Unused, prematch=Prematch} = S,
|
||||
Pair = {Name, elixir_utils:var_context(Meta, Kind)},
|
||||
|
||||
Result =
|
||||
@@ -471,7 +478,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
|
||||
{ok, PairVersion} ->
|
||||
maybe_warn_underscored_var_access(Meta, Name, Kind, E),
|
||||
Var = {Name, [{version, PairVersion} | Meta], Kind},
|
||||
{Var, S#elixir_ex{unused={var_used(Pair, Meta, PairVersion, Unused), Version}}, E};
|
||||
{Var, S#elixir_ex{unused=var_used(Pair, Meta, PairVersion, Unused)}, E};
|
||||
|
||||
Error ->
|
||||
case lists:keyfind(if_undefined, 1, Meta) of
|
||||
@@ -795,8 +802,8 @@ expand_case(Meta, Expr, Opts, S, E) ->
|
||||
false -> Opts
|
||||
end,
|
||||
|
||||
{EOpts, SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE),
|
||||
{{'case', Meta, [EExpr, EOpts]}, SO, EO}.
|
||||
{EOpts, #elixir_ex{version=Counter} = SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE),
|
||||
{{'case', [{version, Counter} | Meta], [EExpr, EOpts]}, SO#elixir_ex{version=Counter+1}, EO}.
|
||||
|
||||
rewrite_case_clauses([{do, [
|
||||
{'->', FalseMeta, [
|
||||
@@ -853,9 +860,9 @@ expand_for({for, Meta, [_ | _] = Args}, S, E, Return) ->
|
||||
{error, Error} -> {file_error(Meta, E, ?MODULE, Error), EOpts}
|
||||
end,
|
||||
|
||||
{{for, Meta, ECases ++ [[{do, EExpr} | NormalizedOpts]]},
|
||||
elixir_env:merge_and_check_unused_vars(SE, S, EE),
|
||||
E}.
|
||||
#elixir_ex{version=Counter} = SF = elixir_env:merge_and_check_unused_vars(SE, S, EE),
|
||||
{{for, [{version, Counter} | Meta], ECases ++ [[{do, EExpr} | NormalizedOpts]]},
|
||||
SF#elixir_ex{version=Counter+1}, E}.
|
||||
|
||||
validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce, Return, Meta, E, Acc) ->
|
||||
validate_for_options(Opts, Pair, Uniq, Reduce, Return, Meta, E, [Pair | Acc]);
|
||||
|
||||
@@ -29,7 +29,8 @@ expand(Meta, Clauses, S, E) when is_list(Clauses) ->
|
||||
|
||||
case lists:usort(EArities) of
|
||||
[_] ->
|
||||
{{fn, Meta, EClauses}, SE, E};
|
||||
#elixir_ex{version=Counter} = SE,
|
||||
{{fn, [{version, Counter} | Meta], EClauses}, SE#elixir_ex{version=Counter+1}, E};
|
||||
_ ->
|
||||
file_error(Meta, E, ?MODULE, clauses_with_different_arities)
|
||||
end.
|
||||
@@ -154,7 +155,7 @@ escape({'&', Meta, [Pos]}, E, Dict) when is_integer(Pos), Pos > 0 ->
|
||||
{Var, Dict};
|
||||
error ->
|
||||
Next = elixir_module:next_counter(?key(E, module)),
|
||||
Var = {capture, [{counter, Next}, {capture, Pos} | Meta], nil},
|
||||
Var = {'_&', [{counter, Next}, {capture, Pos} | Meta], ?MODULE},
|
||||
{Var, orddict:store(Pos, Var, Dict)}
|
||||
end;
|
||||
escape({'&', Meta, [Pos]}, E, _Dict) when is_integer(Pos) ->
|
||||
|
||||
@@ -194,7 +194,8 @@ is_sigil({Name, 2}) ->
|
||||
[H|T] when H >= $A, H =< $Z ->
|
||||
lists:all(fun(L) -> (L >= $0 andalso L =< $9)
|
||||
orelse (L>= $A andalso L =< $Z)
|
||||
end, T)
|
||||
end, T);
|
||||
_ -> false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
@@ -247,7 +248,7 @@ format_error({invalid_import, {Receiver, Name, Arity}}) ->
|
||||
[elixir_aliases:inspect(Receiver), Name, Arity]);
|
||||
|
||||
format_error({invalid_option, only, Value}) ->
|
||||
Message = "invalid :only option for import, expected value to be an atom :functions, :macros"
|
||||
Message = "invalid :only option for import, expected value to be an atom :functions, :macros, :sigils"
|
||||
", or a literal keyword list of function names with arity as values, got: ~s",
|
||||
io_lib:format(Message, ['Elixir.Macro':to_string(Value)]);
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ extract([$\\, $\r, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol,
|
||||
extract([$\\, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
|
||||
extract_nl(Rest, [$\n, $\\ | Buffer], Output, Line, Scope, Interpol, Last);
|
||||
|
||||
extract([$\r, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
|
||||
extract_nl(Rest, [$\n, $\r | Buffer], Output, Line, Scope, Interpol, Last);
|
||||
|
||||
extract([$\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
|
||||
extract_nl(Rest, [$\n | Buffer], Output, Line, Scope, Interpol, Last);
|
||||
|
||||
@@ -81,7 +84,7 @@ extract([$\\ | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) ->
|
||||
%% Catch all clause
|
||||
|
||||
extract([Char1, Char2 | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last)
|
||||
when Char1 =< 255, Char2 =< 255 ->
|
||||
when Char1 =< 255, Char2 =< 255, not (?break(Char1)) ->
|
||||
extract([Char2 | Rest], [Char1 | Buffer], Output, Line, Column + 1, Scope, Interpol, Last);
|
||||
|
||||
extract(Rest, Buffer, Output, Line, Column, Scope, Interpol, Last) ->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
%% SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
-module(elixir_map).
|
||||
-export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/3]).
|
||||
-export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/4]).
|
||||
-import(elixir_errors, [function_error/4, file_error/4, file_warn/4]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
@@ -126,7 +126,7 @@ validate_struct(Atom, _) when is_atom(Atom) -> true;
|
||||
validate_struct(_, _) -> false.
|
||||
|
||||
assert_struct_info_if_not_function(Meta, Name, Assocs, #{function := nil} = E) ->
|
||||
case maybe_load_struct_info(Meta, Name, E) of
|
||||
case maybe_load_struct_info(Meta, Name, hard, E) of
|
||||
{ok, Info} ->
|
||||
[lists:any(fun(Field) -> ?key(Field, field) =:= Key end, Info) orelse
|
||||
function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key})
|
||||
@@ -139,13 +139,22 @@ assert_struct_info_if_not_function(Meta, Name, Assocs, #{function := nil} = E) -
|
||||
assert_struct_info_if_not_function(_Meta, _Name, _Assocs, _E) ->
|
||||
ok.
|
||||
|
||||
maybe_load_struct_info(Meta, Name, E) ->
|
||||
maybe_load_struct_info(Meta, Name, Mode, E) ->
|
||||
try
|
||||
case is_open(Name, Meta, E) andalso lookup_struct_info_from_data_tables(Name) of
|
||||
Lookup = in_context(Name, E) orelse is_compiling_struct(Meta, Name, Mode, E),
|
||||
|
||||
case lookup_struct_info_from_data_tables(Name) of
|
||||
%% If I am accessing myself and there is no attribute,
|
||||
%% don't invoke the fallback to avoid calling loaded code.
|
||||
false when ?key(E, module) =:= Name -> nil;
|
||||
false -> Name:'__info__'(struct);
|
||||
false ->
|
||||
%% If we attempted to lookup a definition, we always invoke it, to deal
|
||||
%% with potential race conditions about the table being deleted at the
|
||||
%% time we were looking up.
|
||||
case Lookup orelse erlang:module_loaded(Name) of
|
||||
true -> Name:'__info__'(struct);
|
||||
false -> nil
|
||||
end;
|
||||
InfoList -> InfoList
|
||||
end
|
||||
of
|
||||
@@ -165,20 +174,8 @@ lookup_struct_info_from_data_tables(Module) ->
|
||||
|
||||
load_struct(Meta, Name, Assocs, E) ->
|
||||
try
|
||||
maybe_load_struct(Meta, Name, Assocs, E)
|
||||
of
|
||||
{ok, Struct} -> Struct;
|
||||
{error, Desc} -> file_error(Meta, E, ?MODULE, Desc)
|
||||
catch
|
||||
Kind:Reason ->
|
||||
Info = [{Name, '__struct__', 1, [{file, "expanding struct"}]},
|
||||
elixir_utils:caller(?line(Meta), ?key(E, file), ?key(E, module), ?key(E, function))],
|
||||
erlang:raise(Kind, Reason, Info)
|
||||
end.
|
||||
|
||||
maybe_load_struct(Meta, Name, Assocs, E) ->
|
||||
try
|
||||
case is_open(Name, Meta, E) andalso elixir_def:external_for(Meta, Name, '__struct__', 1, [def]) of
|
||||
case (in_context(Name, E) orelse is_compiling_struct(Meta, Name, hard, E)) andalso
|
||||
elixir_def:external_for(Meta, Name, '__struct__', 1, [def]) of
|
||||
%% If I am accessing myself and there is no __struct__ function,
|
||||
%% don't invoke the fallback to avoid calling loaded code.
|
||||
false when ?key(E, module) =:= Name ->
|
||||
@@ -207,15 +204,23 @@ maybe_load_struct(Meta, Name, Assocs, E) ->
|
||||
[maps:is_key(Key, Struct) orelse
|
||||
function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key})
|
||||
|| {Key, _} <- Assocs],
|
||||
{ok, Struct};
|
||||
Struct;
|
||||
|
||||
#{'__struct__' := StructName} when is_atom(StructName) ->
|
||||
{error, {struct_name_mismatch, Name, StructName}};
|
||||
Desc = {struct_name_mismatch, Name, StructName},
|
||||
file_error(Meta, E, ?MODULE, Desc);
|
||||
|
||||
Other ->
|
||||
{error, {invalid_struct_return_value, Name, Other}}
|
||||
Desc = {invalid_struct_return_value, Name, Other},
|
||||
file_error(Meta, E, ?MODULE, Desc)
|
||||
catch
|
||||
error:undef -> {error, struct_undef(Name, E)}
|
||||
error:undef ->
|
||||
file_error(Meta, E, ?MODULE, struct_undef(Name, E));
|
||||
|
||||
Kind:Reason ->
|
||||
Info = [{Name, '__struct__', 1, [{file, "expanding struct"}]},
|
||||
elixir_utils:caller(?line(Meta), ?key(E, file), ?key(E, module), ?key(E, function))],
|
||||
erlang:raise(Kind, Reason, Info)
|
||||
end.
|
||||
|
||||
assert_and_trace_struct_assocs(Meta, Name, Assocs, E) ->
|
||||
@@ -226,17 +231,17 @@ assert_and_trace_struct_assocs(Meta, Name, Assocs, E) ->
|
||||
elixir_env:trace({struct_expansion, Meta, Name, Keys}, E),
|
||||
Keys.
|
||||
|
||||
is_open(Name, Meta, E) ->
|
||||
in_context(Name, E) orelse ((code:ensure_loaded(Name) /= {module, Name}) andalso wait_for_struct(Name, Meta, E)).
|
||||
is_compiling_struct(Meta, Name, Mode, E) ->
|
||||
(code:ensure_loaded(Name) /= {module, Name}) andalso wait_for_struct(Meta, Name, Mode, E).
|
||||
|
||||
in_context(Name, E) ->
|
||||
%% We also include the current module because it won't be present
|
||||
%% in context module in case the module name is defined dynamically.
|
||||
lists:member(Name, [?key(E, module) | ?key(E, context_modules)]).
|
||||
|
||||
wait_for_struct(Module, Meta, E) ->
|
||||
wait_for_struct(Meta, Module, Mode, E) ->
|
||||
(erlang:get(elixir_compiler_info) /= undefined) andalso
|
||||
('Elixir.Kernel.ErrorHandler':ensure_compiled(Module, struct, hard, elixir_utils:get_line(Meta, E)) =:= found).
|
||||
('Elixir.Kernel.ErrorHandler':ensure_compiled(Module, struct, Mode, elixir_utils:get_line(Meta, E)) =:= found).
|
||||
|
||||
struct_undef(Name, E) ->
|
||||
case in_context(Name, E) andalso (?key(E, function) == nil) of
|
||||
|
||||
@@ -159,6 +159,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
{DataSet, DataBag} = Tables,
|
||||
|
||||
try
|
||||
CompilerInfo = erlang:get(elixir_compiler_info),
|
||||
put_compiler_modules([Module | CompilerModules]),
|
||||
{Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E),
|
||||
CheckerInfo = checker_info(),
|
||||
@@ -166,6 +167,11 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
|
||||
{Binary, PersistedAttributes, Autoload} =
|
||||
elixir_erl_compiler:spawn(fun() ->
|
||||
case CompilerInfo of
|
||||
undefined -> undefined;
|
||||
_ -> erlang:put(elixir_compiler_info, CompilerInfo)
|
||||
end,
|
||||
|
||||
PersistedAttributes = ets:lookup_element(DataBag, persisted_attributes, 2),
|
||||
Attributes = attributes(DataSet, DataBag, PersistedAttributes),
|
||||
AllDefinitions = elixir_def:fetch_definitions(Module, E),
|
||||
@@ -494,7 +500,7 @@ maybe_prune_versioned_vars(false, _Vars, _Exs, E) ->
|
||||
E;
|
||||
maybe_prune_versioned_vars(true, Vars, ExS, E) ->
|
||||
PruneBefore = length(Vars),
|
||||
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
|
||||
#elixir_ex{vars={ExVars, _}, unused=Unused} = ExS,
|
||||
|
||||
VersionedVars =
|
||||
maps:filter(fun
|
||||
@@ -574,7 +580,7 @@ bag_lookup_element(Table, Name, Pos) ->
|
||||
end.
|
||||
|
||||
beam_location(ModuleAsCharlist) ->
|
||||
case get(elixir_compiler_dest) of
|
||||
case erlang:get(elixir_compiler_dest) of
|
||||
{Dest, Forceload} when is_binary(Dest) ->
|
||||
{filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"),
|
||||
Forceload};
|
||||
@@ -585,7 +591,7 @@ beam_location(ModuleAsCharlist) ->
|
||||
%% Integration with elixir_compiler that makes the module available
|
||||
|
||||
checker_info() ->
|
||||
case get(elixir_checker_info) of
|
||||
case erlang:get(elixir_checker_info) of
|
||||
undefined -> {self(), nil};
|
||||
_ -> 'Elixir.Module.ParallelChecker':get()
|
||||
end.
|
||||
@@ -601,19 +607,19 @@ spawn_parallel_checker(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation)
|
||||
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation, Log).
|
||||
|
||||
make_module_available(Module, Binary, Loaded) ->
|
||||
case get(elixir_module_binaries) of
|
||||
case erlang:get(elixir_module_binaries) of
|
||||
Current when is_list(Current) ->
|
||||
put(elixir_module_binaries, [{Module, Binary} | Current]);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
|
||||
case get(elixir_compiler_info) of
|
||||
case erlang:get(elixir_compiler_info) of
|
||||
undefined ->
|
||||
ok;
|
||||
{PID, _} ->
|
||||
Ref = make_ref(),
|
||||
PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary, Loaded},
|
||||
PID ! {module_available, self(), Ref, erlang:get(elixir_compiler_file), Module, Binary, Loaded},
|
||||
receive {Ref, ack} -> ok end
|
||||
end.
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ end;
|
||||
has_unquotes({{'.', _, [_, unquote]}, _, [_]}, _) -> true;
|
||||
has_unquotes({Var, _, Ctx}, _) when is_atom(Var), is_atom(Ctx) -> false;
|
||||
has_unquotes({Name, _, Args}, QuoteLevel) when is_list(Args) ->
|
||||
has_unquotes(Name) orelse lists:any(fun(Child) -> has_unquotes(Child, QuoteLevel) end, Args);
|
||||
has_unquotes(Name, QuoteLevel) orelse lists:any(fun(Child) -> has_unquotes(Child, QuoteLevel) end, Args);
|
||||
has_unquotes({Left, Right}, QuoteLevel) ->
|
||||
has_unquotes(Left, QuoteLevel) orelse has_unquotes(Right, QuoteLevel);
|
||||
has_unquotes(List, QuoteLevel) when is_list(List) ->
|
||||
@@ -583,8 +583,10 @@ tail_list(Left, Right, Tail) when is_list(Right), is_list(Tail) ->
|
||||
|
||||
tail_list(Left, Right, Tail) when is_list(Left) ->
|
||||
validate_list(Left),
|
||||
[H | T] = lists:reverse(Tail ++ Left),
|
||||
lists:reverse([{'|', [], [H, Right]} | T]).
|
||||
case lists:reverse(Tail ++ Left) of
|
||||
[H | T] -> lists:reverse([{'|', [], [H, Right]} | T]);
|
||||
[] -> argument_error(<<"unquote_splicing/1 failed because it attempted to splice an empty list before tail position">>)
|
||||
end.
|
||||
|
||||
validate_list(List) ->
|
||||
case valid_ast_list(List) of
|
||||
|
||||
@@ -216,6 +216,11 @@ tokenize([$~, H | _T] = Original, Line, Column, Scope, Tokens) when ?is_upcase(H
|
||||
% elixir_errors.erl as by default {char, _, _} tokens are "hijacked" by Erlang
|
||||
% and printed with Erlang syntax ($a) in the parser's error messages.
|
||||
|
||||
%% Reject bare carriage return after ?\ (CR is only valid as part of CRLF line ending).
|
||||
tokenize([$?, $\\, $\r | _Rest] = Original, Line, Column, Scope, Tokens) ->
|
||||
Reason = {?LOC(Line, Column), "invalid bare carriage return after ?\\, use ?\\r instead: ", "\\u000D"},
|
||||
error(Reason, Original, Scope, Tokens);
|
||||
|
||||
tokenize([$?, $\\, H | T], Line, Column, Scope, Tokens) ->
|
||||
Char = elixir_interpolation:unescape_map(H),
|
||||
|
||||
@@ -248,6 +253,11 @@ tokenize([$?, $\\, H | T], Line, Column, Scope, Tokens) ->
|
||||
tokenize(T, Line, Column + 3, NewScope, [Token | Tokens])
|
||||
end;
|
||||
|
||||
%% Reject bare carriage return after ? (CR is only valid as part of CRLF line ending).
|
||||
tokenize([$?, $\r | _Rest] = Original, Line, Column, Scope, Tokens) ->
|
||||
Reason = {?LOC(Line, Column), "invalid bare carriage return after ?, use ?\\r instead: ", "\\u000D"},
|
||||
error(Reason, Original, Scope, Tokens);
|
||||
|
||||
tokenize([$?, Char | T], Line, Column, Scope, Tokens) ->
|
||||
NewScope = case handle_char(Char) of
|
||||
{Escape, Name} ->
|
||||
|
||||
@@ -24,20 +24,23 @@
|
||||
|
||||
%% Bidirectional control
|
||||
%% Retrieved from https://trojansource.codes/trojan-source.pdf
|
||||
-define(bidi(C), C =:= 16#202A;
|
||||
C =:= 16#202B;
|
||||
C =:= 16#202D;
|
||||
C =:= 16#202E;
|
||||
C =:= 16#2066;
|
||||
C =:= 16#2067;
|
||||
C =:= 16#2068;
|
||||
C =:= 16#202C;
|
||||
-define(bidi(C), C =:= 16#202A orelse
|
||||
C =:= 16#202B orelse
|
||||
C =:= 16#202D orelse
|
||||
C =:= 16#202E orelse
|
||||
C =:= 16#2066 orelse
|
||||
C =:= 16#2067 orelse
|
||||
C =:= 16#2068 orelse
|
||||
C =:= 16#202C orelse
|
||||
C =:= 16#2069).
|
||||
|
||||
%% Unsupported newlines
|
||||
%% https://www.unicode.org/reports/tr55/
|
||||
-define(break(C), C =:= 16#000B;
|
||||
C =:= 16#000C;
|
||||
C =:= 16#0085;
|
||||
C =:= 16#2028;
|
||||
%% CR (0x000D) is included: a bare CR not followed by LF is rejected.
|
||||
%% Call sites must handle CRLF explicitly before the ?break check.
|
||||
-define(break(C), C =:= 16#000B orelse
|
||||
C =:= 16#000C orelse
|
||||
C =:= 16#000D orelse
|
||||
C =:= 16#0085 orelse
|
||||
C =:= 16#2028 orelse
|
||||
C =:= 16#2029).
|
||||
@@ -30,7 +30,7 @@ defmodule Code.Normalizer.FormatterASTTest do
|
||||
|
||||
{quoted, comments} = Code.string_to_quoted_with_comments!(good, to_quoted_opts)
|
||||
|
||||
to_algebra_opts = [comments: comments, escape: false] ++ opts
|
||||
to_algebra_opts = Keyword.merge([comments: comments, escape: false], opts)
|
||||
|
||||
quoted
|
||||
|> Code.quoted_to_algebra(to_algebra_opts)
|
||||
@@ -289,13 +289,53 @@ defmodule Code.Normalizer.FormatterASTTest do
|
||||
end
|
||||
|
||||
test "with escaped new lines" do
|
||||
assert_same ~S'''
|
||||
"""
|
||||
one\
|
||||
#{"two"}\
|
||||
three\
|
||||
"""
|
||||
'''
|
||||
source =
|
||||
String.trim(~S'''
|
||||
"""
|
||||
one\
|
||||
#{"two"}\
|
||||
three\
|
||||
"""
|
||||
''')
|
||||
|
||||
assert_same source
|
||||
|
||||
# This is the same default as assert_same
|
||||
assert string_to_string(source, unescape: false, escape: false) == source
|
||||
|
||||
# If we unescape, we cannot keep the original representation
|
||||
# but we must keep it as a valid heredoc
|
||||
assert string_to_string(source, unescape: true, escape: true) ==
|
||||
String.trim(~S'''
|
||||
"""
|
||||
one#{"two"}three
|
||||
"""
|
||||
''')
|
||||
end
|
||||
|
||||
test "with empty escaped new lines" do
|
||||
source =
|
||||
String.trim(~S'''
|
||||
"""
|
||||
one\
|
||||
#{"two"}\
|
||||
three\
|
||||
"""
|
||||
''')
|
||||
|
||||
assert_same source
|
||||
|
||||
# This is the same default as assert_same
|
||||
assert string_to_string(source, unescape: false, escape: false) == source
|
||||
|
||||
# If we unescape, we cannot keep the original representation
|
||||
# but we must keep it as a valid heredoc
|
||||
assert string_to_string(source, unescape: true, escape: true) ==
|
||||
String.trim(~S'''
|
||||
"""
|
||||
one#{"two"}three
|
||||
"""
|
||||
''')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -341,6 +381,31 @@ defmodule Code.Normalizer.FormatterASTTest do
|
||||
'''
|
||||
"""
|
||||
end
|
||||
|
||||
test "with empty escaped new lines" do
|
||||
source =
|
||||
String.trim(~S"""
|
||||
'''
|
||||
one\
|
||||
#{"two"}\
|
||||
three\
|
||||
'''
|
||||
""")
|
||||
|
||||
assert_same source
|
||||
|
||||
# This is the same default as assert_same
|
||||
assert string_to_string(source, unescape: false, escape: false) == source
|
||||
|
||||
# If we unescape, we cannot keep the original representation
|
||||
# but we must keep it as a valid heredoc
|
||||
assert string_to_string(source, unescape: true, escape: true) ==
|
||||
String.trim(~S"""
|
||||
'''
|
||||
one#{"two"}three
|
||||
'''
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
describe "keyword list" do
|
||||
@@ -467,6 +532,23 @@ defmodule Code.Normalizer.FormatterASTTest do
|
||||
'''rsa
|
||||
"""
|
||||
end
|
||||
|
||||
test "with empty escaped new lines" do
|
||||
source =
|
||||
String.trim(~S"""
|
||||
~c'''
|
||||
one\
|
||||
#{"two"}\
|
||||
three\
|
||||
'''
|
||||
""")
|
||||
|
||||
assert_same source
|
||||
|
||||
# Heredoc in sigils always preserves newlines regardless of escape/unescape
|
||||
assert string_to_string(source, unescape: false, escape: false) == source
|
||||
assert string_to_string(source, unescape: true, escape: true) == source
|
||||
end
|
||||
end
|
||||
|
||||
describe "preserves comments formatting" do
|
||||
|
||||
@@ -215,6 +215,33 @@ defmodule CodeTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "prunes internal modules from macro expansion stacktraces" do
|
||||
defmodule PruneStacktraceMacro do
|
||||
defmacro bad do
|
||||
quote do
|
||||
receive 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
stacktrace =
|
||||
try do
|
||||
Code.eval_quoted(
|
||||
quote do
|
||||
require PruneStacktraceMacro
|
||||
PruneStacktraceMacro.bad()
|
||||
end
|
||||
)
|
||||
rescue
|
||||
CompileError -> __STACKTRACE__
|
||||
end
|
||||
|
||||
refute Enum.any?(
|
||||
stacktrace,
|
||||
&match?({mod, _, _, _} when mod in [:elixir_expand, :elixir_dispatch], &1)
|
||||
)
|
||||
end
|
||||
|
||||
test "warns when lexical tracker process is dead" do
|
||||
{pid, ref} = spawn_monitor(fn -> :ok end)
|
||||
assert_receive {:DOWN, ^ref, _, _, _}
|
||||
@@ -261,6 +288,41 @@ defmodule CodeTest do
|
||||
assert {1, _binding} = Code.eval_string("dbg(1)", [])
|
||||
end)
|
||||
end
|
||||
|
||||
test "nested eval preserves outer :dbg_callback" do
|
||||
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
|
||||
|
||||
assert {{2, []}, _binding} =
|
||||
Code.eval_string(
|
||||
"""
|
||||
Code.eval_string("dbg(1)")
|
||||
""",
|
||||
[],
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
test "nested eval preserves outer env in exception stacktrace" do
|
||||
env = %{Code.env_for_eval([]) | file: "outer_file.ex"}
|
||||
|
||||
stacktrace =
|
||||
try do
|
||||
Code.eval_string(
|
||||
"""
|
||||
Code.eval_string("1 + 1")
|
||||
raise "boom"
|
||||
""",
|
||||
[],
|
||||
env
|
||||
)
|
||||
rescue
|
||||
_ -> __STACKTRACE__
|
||||
end
|
||||
|
||||
assert Enum.any?(stacktrace, fn
|
||||
{_, _, _, meta} -> Keyword.get(meta, :file) == ~c"outer_file.ex"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "eval_quoted/1" do
|
||||
@@ -749,13 +811,13 @@ defmodule CodeTest do
|
||||
message = "unknown compiler option: :not_a_valid_option"
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
Code.put_compiler_option(:not_a_valid_option, :foo)
|
||||
Code.put_compiler_option(Process.get(:unused, :not_a_valid_option), :ok)
|
||||
end
|
||||
|
||||
message = "compiler option :debug_info should be a boolean, got: :not_a_boolean"
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
Code.put_compiler_option(:debug_info, :not_a_boolean)
|
||||
Code.put_compiler_option(:debug_info, Process.get(:unused, :not_a_boolean))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -77,8 +77,13 @@ defmodule Config.ProviderTest do
|
||||
assert Provider.validate_config_path!("/foo") == :ok
|
||||
assert Provider.validate_config_path!({:system, "foo", "bar"}) == :ok
|
||||
|
||||
assert_raise ArgumentError, fn -> Provider.validate_config_path!({:system, 1, 2}) end
|
||||
assert_raise ArgumentError, fn -> Provider.validate_config_path!(~c"baz") end
|
||||
assert_raise ArgumentError, fn ->
|
||||
Provider.validate_config_path!(Process.get(:unused, {:system, 1, 2}))
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, fn ->
|
||||
Provider.validate_config_path!(Process.get(:unused, ~c"baz"))
|
||||
end
|
||||
end
|
||||
|
||||
test "resolve!" do
|
||||
|
||||
@@ -52,6 +52,19 @@ defmodule Config.ReaderTest do
|
||||
~r"expected config for app :sample in .*/bad_app.exs to return keyword list",
|
||||
fn -> Config.Reader.read!(fixture_path("configs/bad_app.exs")) end
|
||||
|
||||
assert_raise ArgumentError,
|
||||
~r"expected config .* to be a keyword list of {atom, keyword} pairs",
|
||||
fn ->
|
||||
Config.Reader.eval!(
|
||||
"nofile",
|
||||
~s([{"not_atom", [key: :val]}, {:valid, [k: :v]}])
|
||||
)
|
||||
end
|
||||
|
||||
assert_raise ArgumentError,
|
||||
~r"expected config .* to be a keyword list of {atom, keyword} pairs",
|
||||
fn -> Config.Reader.eval!("nofile", "[:not_a_pair]") end
|
||||
|
||||
assert_raise RuntimeError, "no :env key was given to this configuration file", fn ->
|
||||
Config.Reader.read!(fixture_path("configs/env.exs"))
|
||||
end
|
||||
|
||||
@@ -869,21 +869,26 @@ defmodule FileTest do
|
||||
assert src_mode == dest_mode
|
||||
end
|
||||
|
||||
test "cp_r preserves directory mode" do
|
||||
test "cp_r preserves directory mode with preserve_directory_permissions: true" do
|
||||
src = tmp_path("tmp/src_dir")
|
||||
dest = tmp_path("tmp/dest_dir")
|
||||
inner = Path.join(src, "inner")
|
||||
|
||||
File.mkdir_p!(inner)
|
||||
File.chmod!(inner, 0o700)
|
||||
File.write!(Path.join(inner, "hello"), "world")
|
||||
File.chmod!(inner, 0o500)
|
||||
|
||||
try do
|
||||
File.cp_r!(src, dest)
|
||||
File.cp_r!(src, dest, preserve_directory_permissions: true)
|
||||
|
||||
%File.Stat{mode: src_mode} = File.stat!(inner)
|
||||
%File.Stat{mode: dest_mode} = File.stat!(Path.join(dest, "inner"))
|
||||
assert src_mode == dest_mode
|
||||
|
||||
assert File.read!(Path.join(dest, "inner/hello")) == "world"
|
||||
after
|
||||
File.chmod!(inner, 0o700)
|
||||
File.chmod!(Path.join(dest, "inner"), 0o700)
|
||||
File.rm_rf!(src)
|
||||
File.rm_rf!(dest)
|
||||
end
|
||||
|
||||
@@ -103,6 +103,38 @@ defmodule FloatTest do
|
||||
assert Float.floor(5.0e-324, precision) === 0.0
|
||||
end
|
||||
end
|
||||
|
||||
test "with already-exact floats does not bump" do
|
||||
# The integer rounding step must not add 1 when the truncated remainder
|
||||
# is exactly zero (e.g. -1.5 has no content below the 1st decimal place).
|
||||
assert Float.floor(-1.5, 1) === -1.5
|
||||
assert Float.floor(-1.875, 3) === -1.875
|
||||
assert Float.floor(-3.0, 5) === -3.0
|
||||
end
|
||||
|
||||
test "with extremely small floats" do
|
||||
# `tiny_round`: ceil(+) and floor(-) must bump to ±10^-precision.
|
||||
assert Float.floor(-1.0e-200, 5) === -1.0e-5
|
||||
assert Float.floor(-1.0e-200, 15) === -1.0e-15
|
||||
assert Float.floor(1.0e-200, 5) === 0.0
|
||||
end
|
||||
|
||||
test "with already-integer floats" do
|
||||
# |f| >= 2^52 — fast path returns the float unchanged.
|
||||
assert Float.floor(1.0e20, 3) === 1.0e20
|
||||
assert Float.floor(-1.0e20, 3) === -1.0e20
|
||||
end
|
||||
|
||||
test "with very large floats hits the bignum slow path" do
|
||||
# n = round(|f| * 10^p) >= 2^53 — exercises bignum_to_float / align.
|
||||
# The slow path must round-trip representable floats back to themselves.
|
||||
assert Float.floor(2_661_101_816_343_531.5, 1) === 2_661_101_816_343_531.5
|
||||
assert Float.floor(3.0e15, 1) === 3.0e15
|
||||
assert Float.floor(-3.0e15, 1) === -3.0e15
|
||||
assert Float.floor(1.234e15, 3) === 1.234e15
|
||||
assert Float.floor(1.234567e11, 5) === 1.234567e11
|
||||
assert Float.floor(-1.234567e11, 5) === -1.234567e11
|
||||
end
|
||||
end
|
||||
|
||||
test "ceil/1" do
|
||||
@@ -163,6 +195,32 @@ defmodule FloatTest do
|
||||
assert Float.ceil(-5.0e-324, precision) === -0.0
|
||||
end
|
||||
end
|
||||
|
||||
test "with already-exact floats does not bump" do
|
||||
assert Float.ceil(1.5, 1) === 1.5
|
||||
assert Float.ceil(1.875, 3) === 1.875
|
||||
assert Float.ceil(3.0, 5) === 3.0
|
||||
end
|
||||
|
||||
test "with extremely small floats" do
|
||||
assert Float.ceil(1.0e-200, 5) === 1.0e-5
|
||||
assert Float.ceil(1.0e-200, 15) === 1.0e-15
|
||||
assert Float.ceil(-1.0e-200, 5) === -0.0
|
||||
end
|
||||
|
||||
test "with already-integer floats" do
|
||||
assert Float.ceil(1.0e20, 3) === 1.0e20
|
||||
assert Float.ceil(-1.0e20, 3) === -1.0e20
|
||||
end
|
||||
|
||||
test "with very large floats hits the bignum slow path" do
|
||||
assert Float.ceil(2_661_101_816_343_531.5, 1) === 2_661_101_816_343_531.5
|
||||
assert Float.ceil(3.0e15, 1) === 3.0e15
|
||||
assert Float.ceil(-3.0e15, 1) === -3.0e15
|
||||
assert Float.ceil(1.234e15, 3) === 1.234e15
|
||||
assert Float.ceil(1.234567e11, 5) === 1.234567e11
|
||||
assert Float.ceil(-1.234567e11, 5) === -1.234567e11
|
||||
end
|
||||
end
|
||||
|
||||
describe "round/2" do
|
||||
@@ -203,6 +261,55 @@ defmodule FloatTest do
|
||||
assert Float.round(-5.0e-324, precision) === -0.0
|
||||
end
|
||||
end
|
||||
|
||||
test "rounds up across a digit boundary" do
|
||||
# Rounding pushes the integer answer to 10^precision; the float-emission
|
||||
# step must produce the next-magnitude value cleanly.
|
||||
assert Float.round(0.9995, 3) === 1.0
|
||||
assert Float.round(0.9999, 3) === 1.0
|
||||
assert Float.round(99.9999, 3) === 100.0
|
||||
assert Float.round(-99.9999, 3) === -100.0
|
||||
end
|
||||
|
||||
test "with already-integer floats" do
|
||||
assert Float.round(1.0e20, 3) === 1.0e20
|
||||
assert Float.round(-1.0e20, 3) === -1.0e20
|
||||
assert Float.round(3.0e15, 3) === 3.0e15
|
||||
assert Float.round(-3.0e15, 3) === -3.0e15
|
||||
end
|
||||
|
||||
test "with very large floats hits the bignum slow path" do
|
||||
assert Float.round(3.0e15, 1) === 3.0e15
|
||||
assert Float.round(-3.0e15, 1) === -3.0e15
|
||||
assert Float.round(1.234e15, 3) === 1.234e15
|
||||
assert Float.round(1.234567e11, 5) === 1.234567e11
|
||||
assert Float.round(-1.234567e11, 5) === -1.234567e11
|
||||
|
||||
assert Float.round(2_251_799_813_685_248.5, 1) === 2_251_799_813_685_248.5
|
||||
assert Float.round(2_251_799_813_685_249.5, 1) === 2_251_799_813_685_249.5
|
||||
assert Float.round(-2_251_799_813_685_248.5, 1) === -2_251_799_813_685_248.5
|
||||
assert Float.round(-2_251_799_813_685_249.5, 1) === -2_251_799_813_685_249.5
|
||||
end
|
||||
|
||||
test "preserves documented tie behavior" do
|
||||
# The actual binary representation of 5.5675 is 5.567499999..., so
|
||||
# half-up rounding correctly gives 5.567 (not 5.568).
|
||||
assert Float.round(5.5675, 3) === 5.567
|
||||
assert Float.round(-5.5675, 3) === -5.567
|
||||
assert Float.round(12.5, 0) === 13.0
|
||||
assert Float.round(-12.5, 0) === -13.0
|
||||
end
|
||||
end
|
||||
|
||||
test "round/2, floor/2, and ceil/2 preserve powers of two" do
|
||||
# Powers of two are exactly representable and have no fractional content,
|
||||
# so rounding at any precision must return the input unchanged.
|
||||
for k <- 0..60 do
|
||||
f = :math.pow(2, k)
|
||||
assert Float.round(f, 1) === f
|
||||
assert Float.floor(f, 1) === f
|
||||
assert Float.ceil(f, 1) === f
|
||||
end
|
||||
end
|
||||
|
||||
describe "ratio/1" do
|
||||
|
||||
@@ -268,6 +268,36 @@ defmodule Inspect.TupleTest do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Inspect.NativeRecordTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
@moduletag skip: System.otp_release() < "29"
|
||||
|
||||
@tag :tmp_dir
|
||||
test "basic", %{tmp_dir: tmp_dir} do
|
||||
File.write!("#{tmp_dir}/native_records_test.erl", """
|
||||
-module(native_records_test).
|
||||
-export([test/0]).
|
||||
-record #point{ x = 0.0, y = 0.0 }.
|
||||
|
||||
test() ->
|
||||
#point{}.
|
||||
""")
|
||||
|
||||
{"", 0} = System.cmd("erlc", ["native_records_test.erl"], cd: tmp_dir, stderr_to_stdout: true)
|
||||
|
||||
{:module, _} =
|
||||
:code.load_binary(
|
||||
:native_records_test,
|
||||
~c"nofile",
|
||||
File.read!("#{tmp_dir}/native_records_test.beam")
|
||||
)
|
||||
|
||||
assert inspect(apply(:native_records_test, :test, [])) ==
|
||||
"#native_records_test:point{x = 0.0,y = 0.0}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Inspect.ListTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
|
||||
@@ -196,6 +196,12 @@ defmodule Kernel.BinaryTest do
|
||||
assert_compile_error(message, fn ->
|
||||
Code.eval_string(~s[<<"foo"::float>>])
|
||||
end)
|
||||
|
||||
assert_compile_error(message, fn ->
|
||||
# We need to wrap the example below in a module because
|
||||
# we attempt to continue compilation when inside a function
|
||||
Code.compile_string("defmodule Repro do\n def run, do: <<(<<1>>)::integer>>\nend")
|
||||
end)
|
||||
end
|
||||
|
||||
@bitstring <<"foo", 16::4>>
|
||||
|
||||
@@ -513,6 +513,11 @@ defmodule Kernel.ComprehensionTest do
|
||||
bin = <<8, 1, 32, 2, 3>>
|
||||
assert for(<<s, x::size(s) <- bin>>, into: "", do: <<x::size(s)>>) == <<1>>
|
||||
assert for(<<s, x::size(s) <- bin>>, into: %{}, do: {s, x}) == %{8 => 1}
|
||||
|
||||
# Computed
|
||||
bin = <<1, 1, 2, 2, 3>>
|
||||
assert for(<<s, x::size(s * 8) <- bin>>, into: "", do: <<x::size(s * 8)>>) == <<1, 2, 3>>
|
||||
assert for(<<s, x::size(s * 8) <- bin>>, into: %{}, do: {s, x}) == %{1 => 1, 2 => 515}
|
||||
end
|
||||
|
||||
test "binary for comprehensions where value is not used" do
|
||||
|
||||
@@ -267,6 +267,18 @@ defmodule Kernel.ErrorsTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "invalid literal on bitstring" do
|
||||
assert_compile_error(
|
||||
["nofile:1:1", "a bitstring only accepts binaries, numbers, and variables inside a match"],
|
||||
~c"""
|
||||
<<
|
||||
"foo",
|
||||
[1]
|
||||
>> = List.flatten([])
|
||||
"""
|
||||
)
|
||||
end
|
||||
|
||||
test "literal on map and struct" do
|
||||
assert_compile_error(
|
||||
["nofile:1:10", "expected key-value pairs in a map, got: put_in(foo.bar.baz, nil)"],
|
||||
@@ -662,7 +674,7 @@ defmodule Kernel.ErrorsTest do
|
||||
|
||||
test "function import conflict" do
|
||||
assert_compile_error(
|
||||
["nofile:3:16", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
|
||||
["nofile:3:16", "conflicting exit/1 import from modules :erlang and Kernel"],
|
||||
~c"""
|
||||
defmodule Kernel.ErrorsTest.FunctionImportConflict do
|
||||
import :erlang, only: [exit: 1], warn: false
|
||||
@@ -672,7 +684,7 @@ defmodule Kernel.ErrorsTest do
|
||||
)
|
||||
|
||||
assert_compile_error(
|
||||
["nofile:3:17", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
|
||||
["nofile:3:17", "conflicting exit/1 import from modules :erlang and Kernel"],
|
||||
~c"""
|
||||
defmodule Kernel.ErrorsTest.FunctionImportConflict do
|
||||
import :erlang, only: [exit: 1], warn: false
|
||||
@@ -686,7 +698,7 @@ defmodule Kernel.ErrorsTest do
|
||||
assert_compile_error(
|
||||
[
|
||||
"nofile:3:3",
|
||||
"invalid :only option for import, expected value to be an atom :functions, :macros, or a literal keyword list of function names with arity as values, got: x"
|
||||
"invalid :only option for import, expected value to be an atom :functions, :macros, :sigils, or a literal keyword list of function names with arity as values, got: x"
|
||||
],
|
||||
~c"""
|
||||
defmodule Kernel.ErrorsTest.Only do
|
||||
|
||||
@@ -316,16 +316,6 @@ defmodule Kernel.ExpansionTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "expands vars to local call when :on_undefined_variable is :warn" do
|
||||
Code.put_compiler_option(:on_undefined_variable, :warn)
|
||||
|
||||
{output, env} = expand_env({:a, [], nil}, __ENV__, [])
|
||||
assert output == {:a, [if_undefined: :warn], []}
|
||||
assert Macro.Env.vars(env) == []
|
||||
after
|
||||
Code.put_compiler_option(:on_undefined_variable, :raise)
|
||||
end
|
||||
|
||||
test "expands vars to local call without warning" do
|
||||
env = __ENV__
|
||||
|
||||
@@ -397,8 +387,8 @@ defmodule Kernel.ExpansionTest do
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 1), 1]},
|
||||
{:=, _, [_, var_ver(:x, 1)]}
|
||||
{:=, _, [var_ver(:x, 2), 1]},
|
||||
{:=, _, [_, var_ver(:x, 2)]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
@@ -414,7 +404,7 @@ defmodule Kernel.ExpansionTest do
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 3), 3]}
|
||||
{:=, _, [var_ver(:x, 5), 3]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
@@ -430,7 +420,7 @@ defmodule Kernel.ExpansionTest do
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 2), 2]}
|
||||
{:=, _, [var_ver(:x, 4), 2]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
@@ -1227,8 +1217,8 @@ defmodule Kernel.ExpansionTest do
|
||||
[
|
||||
{:->, [line: 1],
|
||||
[
|
||||
[{:capture, [capture: 1, line: 1], nil}],
|
||||
{:capture, [capture: 1, line: 1], nil}
|
||||
[{:"_&", [capture: 1, line: 1], :elixir_fn}],
|
||||
{:"_&", [capture: 1, line: 1], :elixir_fn}
|
||||
]}
|
||||
]}
|
||||
end
|
||||
|
||||
@@ -180,6 +180,12 @@ defmodule Kernel.ImportTest do
|
||||
defmacro bor(x, _), do: x
|
||||
end
|
||||
|
||||
defmodule ModuleWithInvalidSigils do
|
||||
def sigil_ab(string, []), do: string
|
||||
def sigil_1(string, []), do: string
|
||||
def helper(x), do: x
|
||||
end
|
||||
|
||||
test "import only sigils" do
|
||||
import Kernel, except: [sigil_w: 2]
|
||||
import ModuleWithSigils, only: :sigils
|
||||
@@ -204,6 +210,14 @@ defmodule Kernel.ImportTest do
|
||||
assert ~w(abc def) == ["abc", "def"]
|
||||
end
|
||||
|
||||
test "import only sigils ignores malformed sigil names" do
|
||||
import ModuleWithInvalidSigils, only: :sigils
|
||||
|
||||
assert Macro.Env.lookup_import(__ENV__, {:sigil_ab, 2}) == []
|
||||
assert Macro.Env.lookup_import(__ENV__, {:sigil_1, 2}) == []
|
||||
assert Macro.Env.lookup_import(__ENV__, {:helper, 1}) == []
|
||||
end
|
||||
|
||||
test "import only removes the non-import part" do
|
||||
import List
|
||||
import List, only: :macros
|
||||
|
||||
@@ -16,6 +16,17 @@ defmodule Kernel.MacrosTest.Nested do
|
||||
require Integer
|
||||
end
|
||||
end
|
||||
|
||||
defmacro wrap_empty_var(expr) do
|
||||
empty = Macro.var(:"", nil)
|
||||
|
||||
quote do
|
||||
(fn x ->
|
||||
unquote(empty) = List.wrap(x)
|
||||
unquote(empty)
|
||||
end).(unquote(expr))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Kernel.MacrosTest do
|
||||
@@ -91,4 +102,9 @@ defmodule Kernel.MacrosTest do
|
||||
1
|
||||
end) == 1
|
||||
end
|
||||
|
||||
test "do not collide on empty variable name", config do
|
||||
import Kernel.MacrosTest.Nested
|
||||
assert wrap_empty_var(config) == [config]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1131,6 +1131,121 @@ defmodule Kernel.ParserTest do
|
||||
],
|
||||
~c"\"this is a \\\u2028\""
|
||||
)
|
||||
|
||||
assert_syntax_error(
|
||||
[
|
||||
"nofile:1:12:",
|
||||
"invalid line break character in string: \\u000B. If you want to use such character, use it in its escaped \\u000B form instead"
|
||||
],
|
||||
[34] ++ ~c"this is a " ++ [11, 34]
|
||||
)
|
||||
|
||||
assert_syntax_error(
|
||||
[
|
||||
"nofile:1:12:",
|
||||
"invalid line break character in string: \\u000C. If you want to use such character, use it in its escaped \\u000C form instead"
|
||||
],
|
||||
[34] ++ ~c"this is a " ++ [12, 34]
|
||||
)
|
||||
end
|
||||
|
||||
test "invalid bare carriage return in source" do
|
||||
# Bare CR at top level (already produces an unexpected-token error)
|
||||
assert_syntax_error(
|
||||
["nofile:1:1:", "unexpected token: carriage return (column 1, code point U+000D)"],
|
||||
~c"\r"
|
||||
)
|
||||
|
||||
# CRLF is still a valid line ending at the top level
|
||||
assert Code.string_to_quoted!("x = 1\r\ny = 2") ==
|
||||
{:__block__, [],
|
||||
[
|
||||
{:=, [line: 1], [{:x, [line: 1], nil}, 1]},
|
||||
{:=, [line: 2], [{:y, [line: 2], nil}, 2]}
|
||||
]}
|
||||
|
||||
# Bare CR inside a comment (Trojan Source via comment camouflage)
|
||||
assert_syntax_error(
|
||||
["nofile:1:1:", "invalid line break character in comment: \\u000D"],
|
||||
~c"# safe comment" ++ [13] ++ ~c"hidden_code()"
|
||||
)
|
||||
|
||||
# CRLF correctly terminates a comment (still valid)
|
||||
assert Code.string_to_quoted!("# comment\r\nx = 1") ==
|
||||
{:=, [line: 2], [{:x, [line: 2], nil}, 1]}
|
||||
|
||||
# Bare CR inside a string
|
||||
assert_syntax_error(
|
||||
[
|
||||
"nofile:1:12:",
|
||||
"invalid line break character in string: \\u000D. If you want to use such character, use it in its escaped \\u000D form instead"
|
||||
],
|
||||
[34] ++ ~c"this is a " ++ [13, 34]
|
||||
)
|
||||
|
||||
# Bare CR after backslash inside a string
|
||||
assert_syntax_error(
|
||||
[
|
||||
"nofile:1:13:",
|
||||
"invalid line break character in string: \\u000D. If you want to use such character, use it in its escaped \\u000D form instead"
|
||||
],
|
||||
[34] ++ ~c"this is a " ++ [?\\, 13, 34]
|
||||
)
|
||||
|
||||
# CRLF inside a string is preserved as content (same as before)
|
||||
assert Code.string_to_quoted!([34] ++ ~c"hello" ++ [13, 10] ++ ~c"world" ++ [34]) ==
|
||||
"hello\r\nworld"
|
||||
|
||||
# Bare CR inside a charlist
|
||||
assert_syntax_error(
|
||||
["invalid line break character in string: \\u000D"],
|
||||
[39] ++ ~c"this is a " ++ [13, 39]
|
||||
)
|
||||
|
||||
# Bare CR inside a heredoc
|
||||
assert_syntax_error(
|
||||
["invalid line break character in string: \\u000D"],
|
||||
~c"\"\"\"\nhello" ++ [13] ++ ~c"world\n\"\"\""
|
||||
)
|
||||
|
||||
# Bare CR inside a sigil
|
||||
assert_syntax_error(
|
||||
["invalid line break character in string: \\u000D"],
|
||||
~c"~s(hello" ++ [13] ++ ~c"world)"
|
||||
)
|
||||
|
||||
# Bare CR inside a quoted atom
|
||||
assert_syntax_error(
|
||||
["invalid line break character in string: \\u000D"],
|
||||
~c":\"foo" ++ [13] ++ ~c"bar\""
|
||||
)
|
||||
|
||||
# Bare CR inside a quoted keyword
|
||||
assert_syntax_error(
|
||||
["invalid line break character in string: \\u000D"],
|
||||
~c"[\"foo" ++ [13] ++ ~c"bar\": 1]"
|
||||
)
|
||||
|
||||
# Bare CR inside a quoted call (quoted identifier)
|
||||
assert_syntax_error(
|
||||
["invalid line break character in string: \\u000D"],
|
||||
~c"x.\"foo" ++ [13] ++ ~c"bar\""
|
||||
)
|
||||
|
||||
# Bare CR after ? (char literal)
|
||||
assert_syntax_error(
|
||||
["nofile:1:1:", "invalid bare carriage return after ?"],
|
||||
~c"?" ++ [13]
|
||||
)
|
||||
|
||||
# Bare CR after ?\ (char literal escape)
|
||||
assert_syntax_error(
|
||||
["nofile:1:1:", "invalid bare carriage return after ?\\"],
|
||||
~c"?\\" ++ [13]
|
||||
)
|
||||
|
||||
# ?\r (the proper escape) is still valid
|
||||
assert Code.string_to_quoted!(~c"?\\r") == ?\r
|
||||
end
|
||||
|
||||
test "reserved tokens" do
|
||||
|
||||
@@ -200,10 +200,17 @@ defmodule Kernel.QuoteTest do
|
||||
contents = [1, 2, 3]
|
||||
|
||||
assert quote(do: [unquote_splicing(contents) | [1, 2, 3]]) == [1, 2, 3, 1, 2, 3]
|
||||
|
||||
assert quote(do: [unquote_splicing(contents) | val]) == quote(do: [1, 2, 3 | val])
|
||||
|
||||
assert quote(do: [unquote_splicing(contents) | unquote([4])]) == quote(do: [1, 2, 3, 4])
|
||||
|
||||
# Empty splicing
|
||||
assert quote(do: [unquote_splicing([]) | [1, 2, 3]]) == [1, 2, 3]
|
||||
assert quote(do: [0, unquote_splicing([]) | [1, 2, 3]]) == [0, 1, 2, 3]
|
||||
assert quote(do: [:head, unquote_splicing([]) | :tail]) == quote(do: [:head | :tail])
|
||||
|
||||
assert_raise ArgumentError,
|
||||
~r"unquote_splicing/1 failed because it attempted to splice an empty list",
|
||||
fn -> quote(do: [unquote_splicing([]) | :tail]) end
|
||||
end
|
||||
|
||||
test "splice on stab" do
|
||||
@@ -732,6 +739,15 @@ defmodule Kernel.QuoteTest.HasUnquoteTest do
|
||||
|
||||
refute :elixir_quote.has_unquotes(ast)
|
||||
|
||||
ast =
|
||||
quote unquote: false do
|
||||
quote do
|
||||
unquote(x)()
|
||||
end
|
||||
end
|
||||
|
||||
refute :elixir_quote.has_unquotes(ast)
|
||||
|
||||
ast =
|
||||
quote unquote: false do
|
||||
quote do
|
||||
|
||||
@@ -337,6 +337,22 @@ defmodule Kernel.RaiseTest do
|
||||
assert result == "Erlang error: :sample"
|
||||
end
|
||||
|
||||
test "runtime error from Erlang" do
|
||||
result =
|
||||
try do
|
||||
:erlang.error(%{
|
||||
__struct__: RuntimeError,
|
||||
__exception__: :does_not_matter,
|
||||
message: "oops"
|
||||
})
|
||||
rescue
|
||||
x in [ErlangError] -> {:erlang, Exception.message(x)}
|
||||
x in [RuntimeError] -> {:runtime, Exception.message(x)}
|
||||
end
|
||||
|
||||
assert result == {:runtime, "oops"}
|
||||
end
|
||||
|
||||
test "undefined function error" do
|
||||
result =
|
||||
try do
|
||||
|
||||
@@ -124,16 +124,6 @@ defmodule Kernel.WithTest do
|
||||
assert var == :ok
|
||||
end
|
||||
|
||||
test "errors in with" do
|
||||
assert_raise RuntimeError, fn ->
|
||||
with({:ok, res} <- oops(), do: res)
|
||||
end
|
||||
|
||||
assert_raise RuntimeError, fn ->
|
||||
with({:ok, res} <- ok(42), oops(), do: res)
|
||||
end
|
||||
end
|
||||
|
||||
test "else conditions" do
|
||||
assert (with {:ok, res} <- four() do
|
||||
res
|
||||
@@ -159,18 +149,14 @@ defmodule Kernel.WithTest do
|
||||
end
|
||||
|
||||
defp four() do
|
||||
4
|
||||
Process.get(:unused, 4)
|
||||
end
|
||||
|
||||
defp error() do
|
||||
:error
|
||||
Process.get(:unused, :error)
|
||||
end
|
||||
|
||||
defp ok(num) do
|
||||
{:ok, num}
|
||||
end
|
||||
|
||||
defp oops() do
|
||||
raise("oops")
|
||||
Process.get(:unused, {:ok, num})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -321,8 +321,6 @@ defmodule KernelTest do
|
||||
defp struct_or_map?(arg, name) when is_struct(arg, name) or is_map(arg), do: true
|
||||
defp struct_or_map?(_arg, _name), do: false
|
||||
|
||||
defp not_atom(), do: "not atom"
|
||||
|
||||
test "is_struct/2" do
|
||||
assert delegate_is_struct(%{}, Macro.Env) == false
|
||||
assert delegate_is_struct([], Macro.Env) == false
|
||||
@@ -336,7 +334,7 @@ defmodule KernelTest do
|
||||
assert guarded_is_struct(%{}, Macro.Env) == false
|
||||
|
||||
assert_raise ArgumentError, "argument error", fn ->
|
||||
is_struct(%{}, not_atom())
|
||||
is_struct(%{}, Process.get(:unused, "not atom"))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -417,7 +415,7 @@ defmodule KernelTest do
|
||||
assert guarded_is_exception(%{}, RuntimeError) == false
|
||||
|
||||
assert_raise ArgumentError, "argument error", fn ->
|
||||
delegate_is_exception(%{}, not_atom())
|
||||
delegate_is_exception(%{}, Process.get(:unused, "not atom"))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -594,7 +592,7 @@ defmodule KernelTest do
|
||||
assert map_dot(%{field: true})
|
||||
end
|
||||
|
||||
test "performs all side-effects" do
|
||||
test "performs all side-effects in order" do
|
||||
assert 1 in [1, send(self(), 2)]
|
||||
assert_received 2
|
||||
|
||||
@@ -603,9 +601,17 @@ defmodule KernelTest do
|
||||
|
||||
assert 2 in [1 | send(self(), [2])]
|
||||
assert_received [2]
|
||||
|
||||
send(self(), :first) in send(self(), [:first, :second, :third])
|
||||
assert Process.info(self(), :messages) == {:messages, [:first, [:first, :second, :third]]}
|
||||
assert_received :first
|
||||
assert_received [:first, :second, :third]
|
||||
|
||||
send(self(), 0) in send(self(), -2)..send(self(), 2)//send(self(), 1)
|
||||
assert Process.info(self(), :messages) == {:messages, [0, -2, 2, 1]}
|
||||
end
|
||||
|
||||
test "has proper evaluation order" do
|
||||
test "preserves variable semantics" do
|
||||
a = 1
|
||||
assert 1 in [a = 2, a]
|
||||
# silence unused var warning
|
||||
@@ -1544,6 +1550,16 @@ defmodule KernelTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "raises on durations that result in a negative timeout" do
|
||||
assert_raise ArgumentError,
|
||||
~r"duration must be positive",
|
||||
fn -> to_timeout(Duration.new!(second: -1)) end
|
||||
|
||||
assert_raise ArgumentError,
|
||||
~r"duration must be positive",
|
||||
fn -> to_timeout(Duration.new!(hour: 1, minute: -61)) end
|
||||
end
|
||||
|
||||
test "works with timeouts" do
|
||||
assert to_timeout(1_000) == 1_000
|
||||
assert to_timeout(0) == 0
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule Macro.EnvTest do
|
||||
|
||||
test "with errors" do
|
||||
message =
|
||||
"invalid :only option for import, expected value to be an atom :functions, :macros, " <>
|
||||
"invalid :only option for import, expected value to be an atom :functions, :macros, :sigils, " <>
|
||||
"or a literal keyword list of function names with arity as values, got: "
|
||||
|
||||
assert define_import(env(), meta(), Integer, only: :unknown) ==
|
||||
|
||||
@@ -628,7 +628,7 @@ defmodule MacroTest do
|
||||
|
||||
test "boolean expressions that raise" do
|
||||
falsy = length([]) != 0
|
||||
x = 0
|
||||
x = Process.get(:unused, 0)
|
||||
|
||||
{result, formatted} = dbg_format_no_newline((falsy || x) && 1 / x)
|
||||
|
||||
@@ -1020,7 +1020,6 @@ defmodule MacroTest do
|
||||
with {:ok, width} <- Map.fetch(opts, :width) do
|
||||
width
|
||||
else
|
||||
other when is_integer(other) -> :int
|
||||
other when is_atom(other) -> :atom
|
||||
end
|
||||
)
|
||||
|
||||
@@ -24,6 +24,18 @@ defmodule Module.Types.DescrTest do
|
||||
defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type))
|
||||
defp map_with_default(descr), do: open_map([{to_domain_keys(:term), descr}])
|
||||
|
||||
defp projected_negative_map(size) do
|
||||
Enum.reduce(1..size, open_map(k: open_map(), x: term()), fn index, acc ->
|
||||
difference(
|
||||
acc,
|
||||
open_map([
|
||||
{:k, open_map([{:"value#{index}", integer()}])},
|
||||
{:"field#{index}", integer()}
|
||||
])
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
describe "union" do
|
||||
test "bitmap" do
|
||||
assert union(integer(), float()) == union(float(), integer())
|
||||
@@ -99,6 +111,12 @@ defmodule Module.Types.DescrTest do
|
||||
assert tuple([union(integer(), atom())])
|
||||
|> difference(open_tuple([atom()]))
|
||||
|> equal?(tuple([integer()]))
|
||||
|
||||
# Test union of open tuple
|
||||
# We assert using == on purpose as we want to return open tuples
|
||||
assert union(open_tuple([]), tuple([atom()])) == open_tuple([])
|
||||
assert union(open_tuple([]), difference(open_tuple([]), tuple([atom()]))) == open_tuple([])
|
||||
assert union(difference(open_tuple([]), tuple([atom()])), open_tuple([])) == open_tuple([])
|
||||
end
|
||||
|
||||
test "map" do
|
||||
@@ -109,6 +127,14 @@ defmodule Module.Types.DescrTest do
|
||||
a_integer_open = open_map(a: integer())
|
||||
assert equal?(union(closed_map(a: integer()), a_integer_open), a_integer_open)
|
||||
|
||||
closed = closed_map(a: integer(), b: atom())
|
||||
open = open_map(a: integer(), b: boolean())
|
||||
|
||||
assert subtype?(closed, union(closed, open))
|
||||
assert subtype?(open, union(closed, open))
|
||||
assert subtype?(closed, union(open, closed))
|
||||
assert subtype?(open, union(open, closed))
|
||||
|
||||
# Domain key types
|
||||
atom_to_atom = open_map([{domain_key(:atom), atom()}])
|
||||
atom_to_integer = open_map([{domain_key(:atom), integer()}])
|
||||
@@ -123,16 +149,15 @@ defmodule Module.Types.DescrTest do
|
||||
assert union(atom_to_atom, atom_to_integer)
|
||||
|> subtype?(open_map([{domain_key(:atom), union(atom(), integer())}]))
|
||||
|
||||
# Test unions with empty and open maps
|
||||
# Test unions with empty map
|
||||
assert union(empty_map(), open_map([{domain_key(:integer), atom()}]))
|
||||
|> equal?(open_map([{domain_key(:integer), atom()}]))
|
||||
|
||||
assert union(open_map(), open_map([{domain_key(:integer), atom()}]))
|
||||
|> equal?(open_map())
|
||||
|
||||
# Test union of open map and map with domain key
|
||||
assert union(open_map(), open_map([{domain_key(:integer), atom()}]))
|
||||
|> equal?(open_map())
|
||||
# Test union of open map
|
||||
# We assert using == on purpose as we want to return open maps
|
||||
assert union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map()
|
||||
assert union(open_map(), difference(open_map(), closed_map(foo: atom()))) == open_map()
|
||||
assert union(difference(open_map(), closed_map(foo: atom())), open_map()) == open_map()
|
||||
|
||||
# Ensure no duplicate, no matter the order
|
||||
assert union(
|
||||
@@ -279,6 +304,21 @@ defmodule Module.Types.DescrTest do
|
||||
|
||||
assert intersection(tuple([term(), integer()]), tuple([atom(), term()]))
|
||||
|> equal?(tuple([atom(), integer()]))
|
||||
|
||||
empty_field =
|
||||
closed_map(key: atom([:value]))
|
||||
|> difference(open_map(key: atom(), optional: if_set(atom())))
|
||||
|
||||
assert empty?(empty_field)
|
||||
refute empty_field == none()
|
||||
|
||||
assert intersection(open_tuple([integer()]), tuple([integer(), empty_field]))
|
||||
|> equal?(none())
|
||||
|
||||
assert intersection(tuple([integer(), empty_field]), open_tuple([integer()]))
|
||||
|> equal?(none())
|
||||
|
||||
assert intersection(tuple(), tuple([integer(), empty_field])) |> equal?(none())
|
||||
end
|
||||
|
||||
test "map" do
|
||||
@@ -345,6 +385,10 @@ defmodule Module.Types.DescrTest do
|
||||
closed_map(a: integer(), c: not_set())
|
||||
) ==
|
||||
closed_map(a: integer())
|
||||
|
||||
assert intersection(empty_map(), closed_map(a: if_set(integer()))) == empty_map()
|
||||
assert intersection(closed_map(a: if_set(integer())), empty_map()) == empty_map()
|
||||
refute disjoint?(empty_map(), closed_map(a: if_set(integer())))
|
||||
end
|
||||
|
||||
test "map with domain keys" do
|
||||
@@ -603,6 +647,12 @@ defmodule Module.Types.DescrTest do
|
||||
assert difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none()
|
||||
end
|
||||
|
||||
test "map double negation with redundant empty map" do
|
||||
type = closed_map(a: atom()) |> union(open_map(a: if_set(integer()))) |> union(empty_map())
|
||||
|
||||
assert negation(negation(type)) |> equal?(type)
|
||||
end
|
||||
|
||||
test "map (struct optimizations)" do
|
||||
# We do direct assertions because we want to check how it works underneath
|
||||
atom_foo = atom([:foo])
|
||||
@@ -627,7 +677,7 @@ defmodule Module.Types.DescrTest do
|
||||
closed_map(__struct__: difference(atom(), atom_bar))
|
||||
|
||||
# Explicitly assert we keep it as cascading differences
|
||||
assert %{map: {{:closed, _}, :bdd_bot, :bdd_bot, _}} =
|
||||
assert %{map: {_, {_, :closed, _}, :bdd_bot, :bdd_bot, _}} =
|
||||
difference(
|
||||
difference(
|
||||
open_map(value: term()),
|
||||
@@ -787,7 +837,7 @@ defmodule Module.Types.DescrTest do
|
||||
test "map hoists dynamic" do
|
||||
assert dynamic(open_map(a: integer())) == open_map(a: dynamic(integer()))
|
||||
|
||||
assert dynamic(open_map(a: union(integer(), binary()))) ==
|
||||
assert union(open_map(a: binary()), dynamic(open_map(a: union(integer(), binary())))) ==
|
||||
open_map(a: dynamic(integer()) |> union(binary()))
|
||||
|
||||
# For domains too
|
||||
@@ -798,7 +848,31 @@ defmodule Module.Types.DescrTest do
|
||||
# if_set on dynamic fields also must work
|
||||
t1 = dynamic(open_map(a: if_set(integer())))
|
||||
t2 = open_map(a: if_set(dynamic(integer())))
|
||||
assert t1 == t2
|
||||
assert union(open_map(a: not_set()), t1) == t2
|
||||
end
|
||||
|
||||
test "structural types preserve static part of gradual elements" do
|
||||
static = atom([:ok])
|
||||
gradual = union(static, dynamic(integer()))
|
||||
upper_bound = upper_bound(gradual)
|
||||
x = atom([:x])
|
||||
head = atom([:head])
|
||||
|
||||
for {descr, static_descr, dynamic_descr} <- [
|
||||
{tuple([gradual, x]), tuple([static, x]), tuple([upper_bound, x])},
|
||||
{open_tuple([gradual]), open_tuple([static]), open_tuple([upper_bound])},
|
||||
{closed_map(a: gradual), closed_map(a: static), closed_map(a: upper_bound)},
|
||||
{open_map(a: gradual), open_map(a: static), open_map(a: upper_bound)},
|
||||
{closed_map([{domain_key(:integer), gradual}]),
|
||||
closed_map([{domain_key(:integer), static}]),
|
||||
closed_map([{domain_key(:integer), upper_bound}])},
|
||||
{non_empty_list(gradual), non_empty_list(static), non_empty_list(upper_bound)},
|
||||
{non_empty_list(head, gradual), non_empty_list(head, static),
|
||||
non_empty_list(head, upper_bound)}
|
||||
] do
|
||||
assert descr == Map.put(static_descr, :dynamic, dynamic_descr)
|
||||
assert subtype?(static_descr, descr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1103,26 +1177,26 @@ defmodule Module.Types.DescrTest do
|
||||
|
||||
test "static" do
|
||||
# Full static
|
||||
assert fun_apply(fun(), [integer()]) == {:badarg, [none()]}
|
||||
assert fun_apply(difference(fun(), none_fun(2)), [integer()]) == {:badarg, [none()]}
|
||||
assert fun_apply(fun(), [integer()]) == {:badarg, [none()], false}
|
||||
assert fun_apply(difference(fun(), none_fun(2)), [integer()]) == {:badarg, [none()], false}
|
||||
|
||||
# Basic function application scenarios
|
||||
assert fun_apply(fun([integer()], atom()), [integer()]) == {:ok, atom()}
|
||||
assert fun_apply(fun([integer()], atom()), [float()]) == {:badarg, [integer()]}
|
||||
assert fun_apply(fun([integer()], atom()), [term()]) == {:badarg, [integer()]}
|
||||
assert fun_apply(fun([integer()], atom()), [float()]) == {:badarg, [integer()], false}
|
||||
assert fun_apply(fun([integer()], atom()), [term()]) == {:badarg, [integer()], false}
|
||||
|
||||
# Union argument type: domain is int | float
|
||||
assert fun_apply(fun([union(integer(), float())], atom()), [integer()]) == {:ok, atom()}
|
||||
assert fun_apply(fun([union(integer(), float())], atom()), [float()]) == {:ok, atom()}
|
||||
|
||||
assert fun_apply(fun([union(integer(), float())], atom()), [atom()]) ==
|
||||
{:badarg, [union(integer(), float())]}
|
||||
{:badarg, [union(integer(), float())], false}
|
||||
|
||||
# 2-arity function
|
||||
assert fun_apply(fun([integer(), atom()], binary()), [integer(), atom()]) == {:ok, binary()}
|
||||
|
||||
assert fun_apply(fun([integer(), atom()], binary()), [boolean(), atom()]) ==
|
||||
{:badarg, [integer(), atom()]}
|
||||
{:badarg, [integer(), atom()], false}
|
||||
|
||||
# Return types
|
||||
assert fun_apply(fun([integer()], none()), [integer()]) == {:ok, none()}
|
||||
@@ -1135,7 +1209,9 @@ defmodule Module.Types.DescrTest do
|
||||
|> elem(1)
|
||||
|> equal?(atom())
|
||||
|
||||
assert fun_apply(fun([integer()], atom()), [dynamic(float())]) == {:badarg, [integer()]}
|
||||
assert fun_apply(fun([integer()], atom()), [dynamic(float())]) ==
|
||||
{:badarg, [integer()], false}
|
||||
|
||||
assert fun_apply(fun([integer()], atom()), [dynamic(term())]) == {:ok, dynamic()}
|
||||
|
||||
# Arity mismatches
|
||||
@@ -1198,7 +1274,7 @@ defmodule Module.Types.DescrTest do
|
||||
assert fun_apply(fun, [dynamic(integer())]) == {:ok, union(atom(), dynamic())}
|
||||
assert fun_apply(fun, [dynamic(number())]) == {:ok, dynamic()}
|
||||
assert fun_apply(fun, [integer()]) == {:ok, dynamic()}
|
||||
assert fun_apply(fun, [float()]) == {:badarg, [dynamic(integer())]}
|
||||
assert fun_apply(fun, [float()]) == {:badarg, [dynamic(integer())], false}
|
||||
end
|
||||
|
||||
defp dynamic_fun(args, return), do: dynamic(fun(args, return))
|
||||
@@ -1214,6 +1290,10 @@ defmodule Module.Types.DescrTest do
|
||||
assert fun_apply(dynamic_fun([integer()], atom()), [float()]) == {:ok, dynamic()}
|
||||
assert fun_apply(dynamic_fun([integer()], atom()), [term()]) == {:ok, dynamic()}
|
||||
assert fun_apply(dynamic_fun([integer()], none()), [integer()]) == {:ok, dynamic(none())}
|
||||
|
||||
assert fun_apply(dynamic(fun([integer()], integer())), [none()]) ==
|
||||
{:badarg, [none()], true}
|
||||
|
||||
assert fun_apply(dynamic_fun([integer()], term()), [integer()]) == {:ok, dynamic()}
|
||||
|
||||
# Dynamic return and dynamic args
|
||||
@@ -1329,15 +1409,17 @@ defmodule Module.Types.DescrTest do
|
||||
)
|
||||
|
||||
assert fun_args |> fun_apply([atom()]) == {:ok, dynamic()}
|
||||
assert fun_args |> fun_apply([integer()]) == {:badarg, [dynamic(atom())]}
|
||||
assert fun_args |> fun_apply([integer()]) == {:badarg, [dynamic(atom())], false}
|
||||
|
||||
# ((bool->bool) or dyn(int->int))
|
||||
# booleans work, but not integers
|
||||
fun_mixed_gdom = union(fun([boolean()], boolean()), dynamic_fun([integer()], integer()))
|
||||
assert fun_apply(fun_mixed_gdom, [boolean()]) == {:ok, dynamic()}
|
||||
assert fun_apply(fun_mixed_gdom, [dynamic(boolean())]) == {:ok, union(dynamic(), boolean())}
|
||||
assert fun_apply(fun_mixed_gdom, [integer()]) == {:badarg, [dynamic(boolean())]}
|
||||
assert fun_apply(fun_mixed_gdom, [dynamic(integer())]) == {:badarg, [dynamic(boolean())]}
|
||||
assert fun_apply(fun_mixed_gdom, [integer()]) == {:badarg, [dynamic(boolean())], false}
|
||||
|
||||
assert fun_apply(fun_mixed_gdom, [dynamic(integer())]) ==
|
||||
{:badarg, [dynamic(boolean())], false}
|
||||
|
||||
# Badfun
|
||||
assert union(
|
||||
@@ -1422,6 +1504,16 @@ defmodule Module.Types.DescrTest do
|
||||
refute singleton?(open_tuple([]))
|
||||
refute singleton?(union(tuple([atom([:value])]), tuple([atom([:other_value])])))
|
||||
refute singleton?(union(tuple([atom([:value])]), closed_map(other: atom([:value]))))
|
||||
|
||||
# Both BDD lines produce the same singleton tuple, so the tuple DNF must not duplicate it.
|
||||
a = tuple([union(integer(), atom([:ok])), atom([:x])])
|
||||
b = tuple([integer(), atom([:x, :y])])
|
||||
c = tuple([integer(), union(atom([:x]), binary())])
|
||||
|
||||
t = union(difference(a, b), difference(a, c))
|
||||
# Semantically t ~= {:ok, :x}, confirmed by equal?
|
||||
assert equal?(t, tuple([atom([:ok]), atom([:x])]))
|
||||
assert singleton?(t)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1459,11 +1551,30 @@ defmodule Module.Types.DescrTest do
|
||||
assert truthiness(dynamic(type)) == :always_false
|
||||
end
|
||||
|
||||
assert equal?(
|
||||
intersection(union(atom(), dynamic()), union(atom(), dynamic())),
|
||||
union(atom(), dynamic())
|
||||
)
|
||||
|
||||
for type <-
|
||||
[negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do
|
||||
assert truthiness(type) == :always_true
|
||||
assert truthiness(dynamic(type)) == :always_true
|
||||
end
|
||||
|
||||
assert truthiness(union(atom([true]), integer())) == :always_true
|
||||
|
||||
empty_descr =
|
||||
difference(tuple([number(), integer()]), open_tuple([float(), term()]))
|
||||
|> difference(tuple([integer(), integer()]))
|
||||
|
||||
assert empty?(empty_descr)
|
||||
|
||||
assert truthiness(empty_descr) == :undefined
|
||||
assert truthiness(dynamic(empty_descr)) == :undefined
|
||||
assert truthiness(union(atom([nil]), empty_descr)) == :always_false
|
||||
|
||||
assert truthiness(union(atom([false]), empty_descr)) == :always_false
|
||||
end
|
||||
|
||||
test "atom_fetch" do
|
||||
@@ -1615,6 +1726,7 @@ defmodule Module.Types.DescrTest do
|
||||
assert tuple_fetch(dynamic(empty_tuple()), 0) == :badindex
|
||||
assert tuple_fetch(dynamic(tuple([integer(), atom()])), 2) == :badindex
|
||||
assert tuple_fetch(union(dynamic(), integer()), 0) == :badtuple
|
||||
assert tuple_fetch(tuple([none()]), 0) == :badtuple
|
||||
|
||||
assert tuple_fetch(dynamic(tuple()), 0)
|
||||
|> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end)
|
||||
@@ -1680,6 +1792,7 @@ defmodule Module.Types.DescrTest do
|
||||
assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex
|
||||
assert tuple_insert_at(integer(), 0, boolean()) == :badtuple
|
||||
assert tuple_insert_at(term(), 0, boolean()) == :badtuple
|
||||
assert tuple_insert_at(tuple([none()]), 0, boolean()) == :badtuple
|
||||
|
||||
# Out-of-bounds in a union
|
||||
assert union(tuple([integer(), atom()]), tuple([float()]))
|
||||
@@ -1739,6 +1852,8 @@ defmodule Module.Types.DescrTest do
|
||||
end
|
||||
|
||||
test "tuple_values" do
|
||||
assert tuple_values(term()) == :badtuple
|
||||
assert tuple_values(dynamic()) == dynamic()
|
||||
assert tuple_values(integer()) == :badtuple
|
||||
assert tuple_values(tuple([none()])) == :badtuple
|
||||
assert tuple_values(tuple([])) == none()
|
||||
@@ -2009,6 +2124,11 @@ defmodule Module.Types.DescrTest do
|
||||
|> map_fetch_key(:a) == {false, integer()}
|
||||
end
|
||||
|
||||
# Times out without a projection-only map_fetch_key path
|
||||
test "map_fetch_key with projected negative maps" do
|
||||
assert map_fetch_key(projected_negative_map(100), :k) == {false, open_map()}
|
||||
end
|
||||
|
||||
test "map_fetch_key with dynamic" do
|
||||
assert map_fetch_key(dynamic(), :a) == {true, dynamic()}
|
||||
assert map_fetch_key(union(dynamic(), integer()), :a) == :badmap
|
||||
@@ -2162,6 +2282,55 @@ defmodule Module.Types.DescrTest do
|
||||
map = closed_map([{:a, atom([:a])}, {:__struct__, term()}, {domain_key(:atom), pid()}])
|
||||
{:ok, term} = map_get(map, atom() |> difference(atom([:a])))
|
||||
assert equal?(term, term())
|
||||
|
||||
base = open_map([{domain_key(:atom), term()}])
|
||||
bad = open_map(a: if_set(negation(integer())))
|
||||
map = negation(union(negation(base), bad))
|
||||
|
||||
assert equal?(map, open_map(a: integer()))
|
||||
|
||||
{:ok, type} = map_get(map, atom())
|
||||
assert equal?(type, term())
|
||||
|
||||
{:ok, type} = map_get(map, atom([:a]))
|
||||
assert equal?(type, integer())
|
||||
|
||||
map = closed_map([{:a, term()}, {domain_key(:atom), integer()}])
|
||||
|
||||
{:ok, type} = map_get(map, atom())
|
||||
assert equal?(type, term())
|
||||
|
||||
{:ok, type} = map_get(map, atom([:a]))
|
||||
assert equal?(type, term())
|
||||
|
||||
{:ok, type} = map_get(map, difference(atom(), atom([:a])))
|
||||
assert equal?(type, integer())
|
||||
|
||||
map =
|
||||
closed_map([{:a, term()}, {domain_key(:atom), integer()}])
|
||||
|> difference(open_map(a: negation(pid())))
|
||||
|
||||
{:ok, type} = map_get(map, atom())
|
||||
assert equal?(type, union(integer(), pid()))
|
||||
|
||||
{:ok, type} = map_get(map, atom([:a]))
|
||||
assert equal?(type, pid())
|
||||
|
||||
{:ok, type} = map_get(map, difference(atom(), atom([:a])))
|
||||
assert equal?(type, integer())
|
||||
|
||||
map =
|
||||
closed_map([{:a, term()}, {:b, binary()}, {domain_key(:atom), integer()}])
|
||||
|> difference(open_map(a: negation(pid())))
|
||||
|
||||
{:ok, type} = map_get(map, atom())
|
||||
assert equal?(type, union(union(integer(), pid()), binary()))
|
||||
|
||||
{:ok, type} = map_get(map, atom([:a, :b]))
|
||||
assert equal?(type, union(pid(), binary()))
|
||||
|
||||
{:ok, type} = map_get(map, difference(atom(), atom([:a, :b])))
|
||||
assert equal?(type, integer())
|
||||
end
|
||||
|
||||
test "with lists" do
|
||||
@@ -2190,6 +2359,11 @@ defmodule Module.Types.DescrTest do
|
||||
|
||||
assert map_get(map, list(integer())) == {:ok, atom([:empty, :non_empty])}
|
||||
end
|
||||
|
||||
# Times out without a projection-only map_get path
|
||||
test "with projected negative maps" do
|
||||
assert map_get(projected_negative_map(100), atom([:k])) == {:ok, open_map()}
|
||||
end
|
||||
end
|
||||
|
||||
describe "map_update" do
|
||||
@@ -2275,6 +2449,27 @@ defmodule Module.Types.DescrTest do
|
||||
|> map_update(atom([:b]), integer(), true, true) == {none(), none(), []}
|
||||
end
|
||||
|
||||
# Times out without a projection-aware map_update path
|
||||
test "with projected negative maps" do
|
||||
assert map_update(projected_negative_map(100), atom([:k]), binary()) ==
|
||||
{open_map(), open_map(k: binary(), x: term()), []}
|
||||
end
|
||||
|
||||
test "with non-empty open maps does not call the callback with none from absent branches" do
|
||||
# This is a test of the map_update_fun/5 with forced?: false parameter.
|
||||
# We check that it does not call its typed_fun argument with `none()`
|
||||
# due to the key being absent in the map.
|
||||
type = dynamic(difference(open_map(), empty_map()))
|
||||
|
||||
fun = fn _optional?, value ->
|
||||
send(self(), :callback_invoked)
|
||||
value
|
||||
end
|
||||
|
||||
assert map_update_fun(type, binary(), fun, false, false) == {dynamic(none()), type, []}
|
||||
refute_received :callback_invoked
|
||||
end
|
||||
|
||||
test "with dynamic atom keys" do
|
||||
assert map_update(closed_map(key: atom([:value])), dynamic(), atom([:new_value])) ==
|
||||
{atom([:value]), closed_map(key: atom([:value, :new_value])), []}
|
||||
@@ -2725,6 +2920,31 @@ defmodule Module.Types.DescrTest do
|
||||
])
|
||||
)}
|
||||
end
|
||||
|
||||
# Times out without proper map_put
|
||||
test "with projected negative maps" do
|
||||
map = projected_negative_map(100)
|
||||
|
||||
assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())}
|
||||
|
||||
map = difference(open_map(k: integer(), x: term()), open_map(k: integer(), a: integer()))
|
||||
|
||||
{:ok, type} = map_put(map, atom([:k]), binary())
|
||||
|
||||
assert equal?(
|
||||
type,
|
||||
difference(open_map(k: binary(), x: term()), open_map(k: binary(), a: integer()))
|
||||
)
|
||||
end
|
||||
|
||||
test "with projected negative maps and no popped value projection" do
|
||||
# map_put/3 passes nil as the popped value accumulator because it only needs the map side.
|
||||
map =
|
||||
projected_negative_map(100)
|
||||
|> difference(open_map(k: atom(), x: term()))
|
||||
|
||||
assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())}
|
||||
end
|
||||
end
|
||||
|
||||
describe "disjoint" do
|
||||
@@ -2834,6 +3054,7 @@ defmodule Module.Types.DescrTest do
|
||||
end
|
||||
|
||||
test "list" do
|
||||
assert non_empty_list(none()) |> to_quoted_string() == "none()"
|
||||
assert list(term()) |> to_quoted_string() == "list(term())"
|
||||
assert list(integer()) |> to_quoted_string() == "list(integer())"
|
||||
|
||||
@@ -3027,19 +3248,19 @@ defmodule Module.Types.DescrTest do
|
||||
assert fun([integer()], boolean())
|
||||
|> union(fun([float()], boolean()))
|
||||
|> to_quoted_string() ==
|
||||
"(integer() -> boolean()) or (float() -> boolean())"
|
||||
"(float() -> boolean()) or (integer() -> boolean())"
|
||||
|
||||
assert fun([integer()], boolean())
|
||||
|> intersection(fun([float()], boolean()))
|
||||
|> to_quoted_string() ==
|
||||
"(integer() -> boolean()) and (float() -> boolean())"
|
||||
"(float() -> boolean()) and (integer() -> boolean())"
|
||||
|
||||
# Thanks to lazy BDDs, consecutive union of functions come out as the original union
|
||||
assert fun([integer()], integer())
|
||||
|> union(fun([float()], float()))
|
||||
|> union(fun([pid()], pid()))
|
||||
|> to_quoted_string() ==
|
||||
"(integer() -> integer()) or (float() -> float()) or (pid() -> pid())"
|
||||
"(integer() -> integer()) or (pid() -> pid()) or (float() -> float())"
|
||||
|
||||
assert fun(3) |> to_quoted_string() == "(none(), none(), none() -> term())"
|
||||
|
||||
@@ -3082,14 +3303,14 @@ defmodule Module.Types.DescrTest do
|
||||
|
||||
assert union(domain_part, codomain_part) |> to_quoted_string() ==
|
||||
"""
|
||||
(dynamic(atom()) or integer(), binary() -> float()) or
|
||||
(pid(), float() -> dynamic(atom()) or integer())\
|
||||
(pid(), float() -> dynamic(atom()) or integer()) or
|
||||
(dynamic(atom()) or integer(), binary() -> float())\
|
||||
"""
|
||||
|
||||
assert intersection(domain_part, codomain_part) |> to_quoted_string() ==
|
||||
"""
|
||||
(dynamic(atom()) or integer(), binary() -> float()) and
|
||||
(pid(), float() -> dynamic(atom()) or integer())\
|
||||
(pid(), float() -> dynamic(atom()) or integer()) and
|
||||
(dynamic(atom()) or integer(), binary() -> float())\
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -60,7 +60,10 @@ defmodule Module.Types.ExprTest do
|
||||
|
||||
assert typecheck!([:ok, 123]) == non_empty_list(union(atom([:ok]), integer()))
|
||||
assert typecheck!([:ok | 123]) == non_empty_list(atom([:ok]), integer())
|
||||
assert typecheck!([x], [:ok, x]) == dynamic(non_empty_list(term()))
|
||||
|
||||
assert typecheck!([x], [:ok, x])
|
||||
|> equal?(union(non_empty_list(atom([:ok])), dynamic(non_empty_list(term()))))
|
||||
|
||||
assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term()))
|
||||
end
|
||||
|
||||
@@ -1318,12 +1321,6 @@ defmodule Module.Types.ExprTest do
|
||||
|
||||
dynamic()
|
||||
|
||||
where "capture" was given the type:
|
||||
|
||||
# type: dynamic()
|
||||
# from: types_test.ex:LINE
|
||||
&1
|
||||
|
||||
instead of using &1, you must define an anonymous function, define a variable and pattern match on "%Date{}"
|
||||
"""
|
||||
|
||||
@@ -1425,6 +1422,20 @@ defmodule Module.Types.ExprTest do
|
||||
assert typecheck!([x = 123, y = 456.0], x == y) == boolean()
|
||||
end
|
||||
|
||||
test "preserves static components across gradual self-intersections" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
# y :: :foo or dynamic()
|
||||
y = if :rand.uniform() > 0.5, do: :foo, else: x
|
||||
# y's type intersected with itself
|
||||
y = y
|
||||
|
||||
y
|
||||
)
|
||||
) == union(atom([:foo]), dynamic())
|
||||
end
|
||||
|
||||
test "in dynamic mode" do
|
||||
assert typedyn!([x = 123, y = 456.0], x < y) == dynamic(boolean())
|
||||
assert typedyn!([x = 123, y = 456.0], x == y) == dynamic(boolean())
|
||||
@@ -1596,6 +1607,34 @@ defmodule Module.Types.ExprTest do
|
||||
# from: types_test.ex:LINE-1
|
||||
y = 123
|
||||
"""
|
||||
|
||||
assert typewarn!(
|
||||
[x],
|
||||
cond do
|
||||
if(x, do: true, else: 1) -> :if
|
||||
x -> :x
|
||||
end
|
||||
) ==
|
||||
{atom([:if, :x]),
|
||||
~l"""
|
||||
this clause in cond will always match:
|
||||
|
||||
if x do
|
||||
true
|
||||
else
|
||||
1
|
||||
end
|
||||
|
||||
since it has type:
|
||||
|
||||
true or integer()
|
||||
|
||||
where "x" was given the type:
|
||||
|
||||
# type: dynamic()
|
||||
# from: types_test.ex:LINE-7
|
||||
x
|
||||
"""}
|
||||
end
|
||||
|
||||
test "Integer.to_string/1" do
|
||||
@@ -1812,6 +1851,142 @@ defmodule Module.Types.ExprTest do
|
||||
atom([:non_empty_map, :maybe_empty_map])
|
||||
end
|
||||
|
||||
test "consider external variables as not precise" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
res =
|
||||
case System.get_env("foo") do
|
||||
nil when x == :foo -> {:first, x}
|
||||
rest -> {:second, rest}
|
||||
end
|
||||
|
||||
{x, res}
|
||||
)
|
||||
) ==
|
||||
dynamic(
|
||||
tuple([
|
||||
term(),
|
||||
union(
|
||||
tuple([atom([:first]), atom([:foo])]),
|
||||
tuple([atom([:second]), union(binary(), atom([nil]))])
|
||||
)
|
||||
])
|
||||
)
|
||||
end
|
||||
|
||||
test "refine types when there are dead branches (conditional)" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
if is_integer(x) do
|
||||
raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(negation(integer()))
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
if x == :foo do
|
||||
raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(negation(atom([:foo])))
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
if is_map(x) or is_integer(x) do
|
||||
raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(negation(union(open_map(), integer())))
|
||||
|
||||
# When it is not precise enough, we don't filter
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
if x == "foo" do
|
||||
raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic()
|
||||
end
|
||||
|
||||
test "refine types when there are dead branches (case)" do
|
||||
assert typecheck!(
|
||||
[x, key],
|
||||
(
|
||||
case x do
|
||||
%{^key => value} -> {:ok, value}
|
||||
%{} -> raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(open_map())
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
case x do
|
||||
%{foo: _} -> :ok
|
||||
%{} -> raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(open_map(foo: term()))
|
||||
|
||||
assert typecheck!(
|
||||
[x, key],
|
||||
(
|
||||
case x do
|
||||
%{foo: _} -> :ok
|
||||
%{^key => value} -> {:value, value}
|
||||
%{} -> raise "bad"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(open_map())
|
||||
end
|
||||
|
||||
test "refine types when it is dynamic" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
case x do
|
||||
{} -> :tuple
|
||||
%{} -> :empty_list
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic()
|
||||
|
||||
assert typedyn!(
|
||||
[x],
|
||||
(
|
||||
case x do
|
||||
{} -> :tuple
|
||||
%{} -> :empty_list
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(union(open_map(), tuple([])))
|
||||
end
|
||||
|
||||
test "warns on redundant clauses" do
|
||||
assert typewarn!(
|
||||
[x],
|
||||
@@ -1915,6 +2090,52 @@ defmodule Module.Types.ExprTest do
|
||||
) == atom([:ok, nil])
|
||||
end
|
||||
|
||||
test "refines expression type" do
|
||||
assert typecheck!(
|
||||
if x = System.get_env("HELLO") do
|
||||
{:ok, x}
|
||||
else
|
||||
{:error, x}
|
||||
end
|
||||
) ==
|
||||
dynamic(
|
||||
union(
|
||||
tuple([atom([:ok]), binary()]),
|
||||
tuple([atom([:error]), atom([nil])])
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "refines nested expression type" do
|
||||
assert typecheck!(
|
||||
case (if x = System.get_env("HELLO") do
|
||||
:do
|
||||
else
|
||||
:else
|
||||
end) do
|
||||
:do -> {:ok, x}
|
||||
:else -> {:error, x}
|
||||
end
|
||||
) ==
|
||||
dynamic(
|
||||
union(
|
||||
tuple([atom([:ok]), binary()]),
|
||||
tuple([atom([:error]), atom([nil])])
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "discards warnings from refinements" do
|
||||
assert {_, [_]} =
|
||||
typediag!(
|
||||
if x = System.unknown_function_get_env("HELLO") do
|
||||
{:ok, x}
|
||||
else
|
||||
{:error, x}
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
test "and/or does not report on literals" do
|
||||
assert typecheck!(false and true) == boolean()
|
||||
assert typecheck!(false or true) == atom([true])
|
||||
@@ -2264,11 +2485,7 @@ defmodule Module.Types.ExprTest do
|
||||
|
||||
:error ->
|
||||
|
||||
because it attempts to match on the result of:
|
||||
|
||||
Atom.to_string(x)
|
||||
|
||||
which has type:
|
||||
it is expected to match on type:
|
||||
|
||||
binary()
|
||||
"""
|
||||
@@ -2287,12 +2504,12 @@ defmodule Module.Types.ExprTest do
|
||||
union(
|
||||
closed_map(
|
||||
__struct__: atom([ArgumentError]),
|
||||
__exception__: atom([true]),
|
||||
__exception__: term(),
|
||||
message: term()
|
||||
),
|
||||
closed_map(
|
||||
__struct__: atom([RuntimeError]),
|
||||
__exception__: atom([true]),
|
||||
__exception__: term(),
|
||||
message: term()
|
||||
)
|
||||
)
|
||||
@@ -2309,7 +2526,7 @@ defmodule Module.Types.ExprTest do
|
||||
) ==
|
||||
open_map(
|
||||
__struct__: atom(),
|
||||
__exception__: atom([true])
|
||||
__exception__: term()
|
||||
)
|
||||
end
|
||||
|
||||
@@ -2328,7 +2545,7 @@ defmodule Module.Types.ExprTest do
|
||||
|
||||
given types:
|
||||
|
||||
%{..., __exception__: true, __struct__: atom()}
|
||||
%{..., __exception__: term(), __struct__: atom()}
|
||||
|
||||
but expected one of:
|
||||
|
||||
@@ -2336,7 +2553,7 @@ defmodule Module.Types.ExprTest do
|
||||
|
||||
where "e" was given the type:
|
||||
|
||||
# type: %{..., __exception__: true, __struct__: atom()}
|
||||
# type: %{..., __exception__: term(), __struct__: atom()}
|
||||
# from: types_test.ex
|
||||
rescue e
|
||||
|
||||
@@ -2462,6 +2679,90 @@ defmodule Module.Types.ExprTest do
|
||||
"""}
|
||||
end
|
||||
|
||||
test "refines types (2 clauses)" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
cond do
|
||||
is_binary(x) -> {:first, x}
|
||||
true -> {:second, x}
|
||||
end
|
||||
) ==
|
||||
dynamic(
|
||||
union(
|
||||
tuple([atom([:first]), binary()]),
|
||||
tuple([atom([:second]), negation(binary())])
|
||||
)
|
||||
)
|
||||
|
||||
# Negated types do not leak through
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
cond do
|
||||
is_binary(x) -> {:first, x}
|
||||
true -> {:second, x}
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic()
|
||||
|
||||
# Unless one of them raise
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
cond do
|
||||
is_binary(x) -> raise "oops"
|
||||
true -> :ok
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(negation(binary()))
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
cond do
|
||||
is_binary(x) -> :ok
|
||||
true -> raise "oops"
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic(binary())
|
||||
end
|
||||
|
||||
test "refines types (3+ clauses)" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
cond do
|
||||
is_binary(x) -> {:first, x}
|
||||
is_atom(x) -> {:second, x}
|
||||
true -> {:third, x}
|
||||
end
|
||||
) ==
|
||||
dynamic(
|
||||
tuple([atom([:first]), binary()])
|
||||
|> union(tuple([atom([:second]), atom()]))
|
||||
|> union(tuple([atom([:third]), negation(union(binary(), atom()))]))
|
||||
)
|
||||
|
||||
# Negated types do not leak through
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
cond do
|
||||
is_binary(x) -> {:first, x}
|
||||
is_atom(x) -> {:second, x}
|
||||
true -> {:third, x}
|
||||
end
|
||||
|
||||
x
|
||||
)
|
||||
) == dynamic()
|
||||
end
|
||||
|
||||
test "resets branches" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
@@ -2572,6 +2873,24 @@ defmodule Module.Types.ExprTest do
|
||||
) == union(bitstring(), list(term()))
|
||||
end
|
||||
|
||||
test ":into inference" do
|
||||
assert typecheck!(
|
||||
[x, y],
|
||||
(
|
||||
List.to_integer([_ | _] = for(_ <- x, do: y))
|
||||
y
|
||||
)
|
||||
) == dynamic(integer())
|
||||
|
||||
assert typecheck!(
|
||||
[x, y],
|
||||
(
|
||||
for(<<_ <- x>>, do: y, into: "")
|
||||
y
|
||||
)
|
||||
) == dynamic(bitstring())
|
||||
end
|
||||
|
||||
test ":into incompatibility" do
|
||||
assert typeerror!([binary], for(<<x <- binary>>, do: x, into: "")) =~ ~l"""
|
||||
expected the body of a for-comprehension with into: binary() (or bitstring()) to be a binary (or bitstring):
|
||||
@@ -2635,6 +2954,77 @@ defmodule Module.Types.ExprTest do
|
||||
end
|
||||
|
||||
describe "with" do
|
||||
test "computes non-matched types" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
with y when is_binary(y) <- System.get_env(x) do
|
||||
{:ok, y}
|
||||
end
|
||||
) == dynamic(union(tuple([atom([:ok]), binary()]), atom([nil])))
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
with "not precise" <- System.get_env(x) do
|
||||
:not_precise
|
||||
end
|
||||
) ==
|
||||
dynamic(union(binary(), atom([nil, :not_precise])))
|
||||
|> union(atom([:not_precise]))
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
with :ok <- System.get_env(x) && :ok do
|
||||
{:ok, x}
|
||||
end
|
||||
) ==
|
||||
dynamic(union(tuple([atom([:ok]), binary()]), atom([nil])))
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
with {:ok, "not precise"} <- if(y = System.get_env(x), do: {:ok, y}) do
|
||||
{:ok, x}
|
||||
end
|
||||
) ==
|
||||
dynamic(union(tuple([atom([:ok]), binary()]), atom([nil])))
|
||||
|> union(atom([nil]))
|
||||
end
|
||||
|
||||
test "warns on non-matching generators" do
|
||||
assert typeerror!(
|
||||
[x],
|
||||
with :ok <- System.get_env(x) do
|
||||
x
|
||||
end
|
||||
) =~ """
|
||||
the following pattern will never match:
|
||||
|
||||
:ok <- System.get_env(x)
|
||||
|
||||
because the right-hand side has type:
|
||||
|
||||
dynamic(nil or binary())
|
||||
"""
|
||||
end
|
||||
|
||||
test "warns on non-matching clauses in else" do
|
||||
assert typeerror!(
|
||||
[x],
|
||||
with y when is_binary(y) <- System.get_env(x) do
|
||||
x
|
||||
else
|
||||
:ok -> :ok
|
||||
end
|
||||
) == ~l"""
|
||||
the following clause will never match:
|
||||
|
||||
:ok ->
|
||||
|
||||
it is expected to match on type:
|
||||
|
||||
dynamic(nil)
|
||||
"""
|
||||
end
|
||||
|
||||
test "warns on redundant clauses in else" do
|
||||
assert typewarn!(
|
||||
[x],
|
||||
|
||||
@@ -29,7 +29,7 @@ defmodule Module.Types.InferTest do
|
||||
for {fun, %{sig: sig}} <- data.exports, into: %{}, do: {fun, sig}
|
||||
end
|
||||
|
||||
test "infer types from patterns", config do
|
||||
test "from patterns", config do
|
||||
types =
|
||||
infer config do
|
||||
def fun1(%y{}, %x{}, x = y, x = Point), do: :ok
|
||||
@@ -51,32 +51,7 @@ defmodule Module.Types.InferTest do
|
||||
assert types[{:fun4, 4}] == {:infer, nil, [{args, atom([:ok])}]}
|
||||
end
|
||||
|
||||
test "infer types from expressions", config do
|
||||
types =
|
||||
infer config do
|
||||
def fun(x) do
|
||||
x.foo + x.bar
|
||||
end
|
||||
end
|
||||
|
||||
number = union(integer(), float())
|
||||
|
||||
assert types[{:fun, 1}] ==
|
||||
{:infer, nil, [{[open_map(foo: number, bar: number)], dynamic(number)}]}
|
||||
end
|
||||
|
||||
test "infer with Elixir built-in", config do
|
||||
types =
|
||||
infer config do
|
||||
def parse(string), do: Integer.parse(string)
|
||||
end
|
||||
|
||||
assert types[{:parse, 1}] ==
|
||||
{:infer, nil,
|
||||
[{[term()], dynamic(union(atom([:error]), tuple([integer(), binary()])))}]}
|
||||
end
|
||||
|
||||
test "merges patterns", config do
|
||||
test "from patterns (with merging)", config do
|
||||
types =
|
||||
infer config do
|
||||
def fun(:ok), do: :one
|
||||
@@ -95,7 +70,59 @@ defmodule Module.Types.InferTest do
|
||||
]}
|
||||
end
|
||||
|
||||
test "infers return types from private functions", config do
|
||||
test "from patterns (with overlapping simplifications)", config do
|
||||
types =
|
||||
infer config do
|
||||
# Test that we don't return differences on catch-all
|
||||
def foo(%{foo: 123}, {:ok, _}), do: 1
|
||||
def foo(%{}, {_, _}), do: 2
|
||||
|
||||
def bar(%{foo: 123}, {:ok, _}), do: 1
|
||||
def bar(map, tuple), do: {map, tuple}
|
||||
|
||||
# Notice that we will return a broader domain than the inferred types
|
||||
# but because the domain is only used for refining reverse arrows,
|
||||
# type checking will still fail if %{foo: not integer()} is given
|
||||
def baz(%{foo: var}), do: Integer.to_string(var)
|
||||
def baz(%{}), do: :error
|
||||
end
|
||||
|
||||
assert {:infer, domain, _} = types[{:foo, 2}]
|
||||
assert domain == [open_map(), tuple([term(), term()])]
|
||||
|
||||
assert {:infer, domain, _} = types[{:bar, 2}]
|
||||
assert domain == [term(), term()]
|
||||
|
||||
assert {:infer, domain, _} = types[{:baz, 1}]
|
||||
assert domain == [open_map()]
|
||||
end
|
||||
|
||||
test "from expressions", config do
|
||||
types =
|
||||
infer config do
|
||||
def fun(x) do
|
||||
x.foo + x.bar
|
||||
end
|
||||
end
|
||||
|
||||
number = union(integer(), float())
|
||||
|
||||
assert types[{:fun, 1}] ==
|
||||
{:infer, nil, [{[open_map(foo: number, bar: number)], dynamic(number)}]}
|
||||
end
|
||||
|
||||
test "from Elixir built-in", config do
|
||||
types =
|
||||
infer config do
|
||||
def parse(string), do: Integer.parse(string)
|
||||
end
|
||||
|
||||
assert types[{:parse, 1}] ==
|
||||
{:infer, nil,
|
||||
[{[term()], dynamic(union(atom([:error]), tuple([integer(), binary()])))}]}
|
||||
end
|
||||
|
||||
test "from private functions", config do
|
||||
types =
|
||||
infer config do
|
||||
def pub(x), do: priv(x)
|
||||
@@ -109,20 +136,21 @@ defmodule Module.Types.InferTest do
|
||||
assert types[{:priv, 1}] == nil
|
||||
end
|
||||
|
||||
test "infers return types from super functions", config do
|
||||
test "from super functions", config do
|
||||
types =
|
||||
infer config do
|
||||
def pub(:ok), do: :ok
|
||||
def pub(:error), do: :error
|
||||
defoverridable pub: 1
|
||||
def pub(x), do: super(x)
|
||||
def pub(x), do: {:ok, super(x)}
|
||||
end
|
||||
|
||||
assert types[{:pub, 1}] ==
|
||||
{:infer, nil, [{[atom([:ok, :error])], dynamic(atom([:ok, :error]))}]}
|
||||
{:infer, nil,
|
||||
[{[atom([:ok, :error])], dynamic(tuple([atom([:ok]), atom([:ok, :error])]))}]}
|
||||
end
|
||||
|
||||
test "infers return types even with loops", config do
|
||||
test "from cyclic calls", config do
|
||||
types =
|
||||
infer config do
|
||||
def pub(x), do: pub(x)
|
||||
@@ -130,4 +158,158 @@ defmodule Module.Types.InferTest do
|
||||
|
||||
assert types[{:pub, 1}] == {:infer, nil, [{[term()], dynamic()}]}
|
||||
end
|
||||
|
||||
test "from structs", config do
|
||||
types =
|
||||
infer config do
|
||||
defstruct [:x, :y, :z]
|
||||
|
||||
def struct_create_with_atom_keys(x) do
|
||||
infer(y = %__MODULE__{x: x})
|
||||
{x, y}
|
||||
end
|
||||
|
||||
def map_create_with_atom_keys(x) do
|
||||
infer(%{__struct__: __MODULE__, x: x, y: nil, z: nil})
|
||||
x
|
||||
end
|
||||
|
||||
def map_update_with_atom_keys(x) do
|
||||
infer(%{x | y: nil})
|
||||
x
|
||||
end
|
||||
|
||||
def map_update_with_unknown_keys(x, key) do
|
||||
infer(%{x | key => 123})
|
||||
x
|
||||
end
|
||||
|
||||
defp infer(%__MODULE__{x: <<_::binary>>, y: nil}) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
module = config.test
|
||||
|
||||
assert {:infer, _, [{_, return}]} = types[{:struct_create_with_atom_keys, 1}]
|
||||
|
||||
assert return ==
|
||||
dynamic(
|
||||
tuple([
|
||||
binary(),
|
||||
closed_map(
|
||||
__struct__: atom([module]),
|
||||
x: binary(),
|
||||
y: atom([nil]),
|
||||
z: atom([nil])
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
assert {:infer, _, [{_, return}]} = types[{:map_create_with_atom_keys, 1}]
|
||||
assert return == dynamic(binary())
|
||||
|
||||
assert {:infer, _, [{_, return}]} = types[{:map_update_with_atom_keys, 1}]
|
||||
|
||||
assert return ==
|
||||
dynamic(
|
||||
closed_map(
|
||||
__struct__: atom([module]),
|
||||
x: binary(),
|
||||
y: atom([nil]),
|
||||
z: term()
|
||||
)
|
||||
)
|
||||
|
||||
assert {:infer, _, [{_, return}]} = types[{:map_update_with_unknown_keys, 2}]
|
||||
|
||||
assert return ==
|
||||
dynamic(
|
||||
closed_map(
|
||||
__struct__: atom([module]),
|
||||
x: binary(),
|
||||
y: atom([nil]),
|
||||
z: term()
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "from captures", config do
|
||||
types =
|
||||
infer config do
|
||||
def captured, do: &to_capture/1
|
||||
defp to_capture(<<"ok">>), do: :ok
|
||||
defp to_capture(<<"error">>), do: :error
|
||||
defp to_capture([_ | _]), do: :list
|
||||
end
|
||||
|
||||
assert {:infer, _, [{_, return}]} = types[{:captured, 0}]
|
||||
|
||||
assert equal?(
|
||||
return,
|
||||
fun_from_non_overlapping_clauses([
|
||||
{[binary()], dynamic(atom([:ok, :error]))},
|
||||
{[non_empty_list(term(), term())], dynamic(atom([:list]))}
|
||||
])
|
||||
)
|
||||
end
|
||||
|
||||
test "from defaults", config do
|
||||
types =
|
||||
infer config do
|
||||
# We should only stick with the clause we use
|
||||
def filtered_ok(x \\ :ok, arg)
|
||||
def filtered_ok(:ok, arg) when is_binary(arg), do: {:ok_binary, arg}
|
||||
def filtered_ok(:ok, arg) when is_atom(arg), do: {:ok_atom, arg}
|
||||
def filtered_ok(:error, arg), do: {:error, arg}
|
||||
|
||||
# Without polymorphic types, we still return arg as term
|
||||
def ok_or_error(x, arg \\ 0)
|
||||
def ok_or_error(:ok, arg), do: {:ok, arg}
|
||||
def ok_or_error(:error, arg), do: {:error, arg}
|
||||
end
|
||||
|
||||
assert {:infer, _, clauses} = types[{:filtered_ok, 1}]
|
||||
|
||||
assert clauses == [
|
||||
{[binary()], dynamic(tuple([atom([:ok_binary]), binary()]))},
|
||||
{[atom()], dynamic(tuple([atom([:ok_atom]), atom()]))}
|
||||
]
|
||||
|
||||
assert {:infer, _, clauses} = types[{:ok_or_error, 1}]
|
||||
|
||||
assert clauses == [
|
||||
{[atom([:ok])], dynamic(tuple([atom([:ok]), term()]))},
|
||||
{[atom([:error])], dynamic(tuple([atom([:error]), term()]))}
|
||||
]
|
||||
end
|
||||
|
||||
test "from defaults (regression with multiple clauses)", config do
|
||||
types =
|
||||
infer config do
|
||||
def entries(tree_node, opts \\ [keep_text: true])
|
||||
|
||||
def entries({_, _, subentries}, keep_text: false) do
|
||||
Enum.filter(subentries, &is_tuple/1)
|
||||
end
|
||||
|
||||
def entries({_, _, subentries}, keep_text: _) do
|
||||
subentries
|
||||
end
|
||||
|
||||
def entries({_, _, _} = tree_node, opts) do
|
||||
opts = Keyword.validate!(opts, keep_text: true)
|
||||
entries(tree_node, keep_text: opts[:keep_text])
|
||||
end
|
||||
|
||||
def entries(_tree_node, _opts), do: nil
|
||||
end
|
||||
|
||||
{:infer, _, signature} = types[{:entries, 1}]
|
||||
|
||||
assert signature == [
|
||||
{[tuple([term(), term(), term()])], dynamic()},
|
||||
{[negation(tuple([term(), term(), term()]))], atom([nil])}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -85,114 +85,6 @@ defmodule Module.Types.IntegrationTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "writes exports with inferred map types" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
defstruct [:x, :y, :z]
|
||||
|
||||
def struct_create_with_atom_keys(x) do
|
||||
infer(y = %A{x: x})
|
||||
{x, y}
|
||||
end
|
||||
|
||||
def map_create_with_atom_keys(x) do
|
||||
infer(%{__struct__: A, x: x, y: nil, z: nil})
|
||||
x
|
||||
end
|
||||
|
||||
def map_update_with_atom_keys(x) do
|
||||
infer(%{x | y: nil})
|
||||
x
|
||||
end
|
||||
|
||||
def map_update_with_unknown_keys(x, key) do
|
||||
infer(%{x | key => 123})
|
||||
x
|
||||
end
|
||||
|
||||
defp infer(%A{x: <<_::binary>>, y: nil}) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
modules = compile_modules(files)
|
||||
exports = read_chunk(modules[A]).exports |> Map.new()
|
||||
|
||||
return = fn name, arity ->
|
||||
pair = {name, arity}
|
||||
%{^pair => %{sig: {:infer, nil, [{_, return}]}}} = exports
|
||||
return
|
||||
end
|
||||
|
||||
assert return.(:struct_create_with_atom_keys, 1) ==
|
||||
dynamic(
|
||||
tuple([
|
||||
binary(),
|
||||
closed_map(
|
||||
__struct__: atom([A]),
|
||||
x: binary(),
|
||||
y: atom([nil]),
|
||||
z: atom([nil])
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
assert return.(:map_create_with_atom_keys, 1) == dynamic(binary())
|
||||
|
||||
assert return.(:map_update_with_atom_keys, 1) ==
|
||||
dynamic(
|
||||
closed_map(
|
||||
__struct__: atom([A]),
|
||||
x: binary(),
|
||||
y: atom([nil]),
|
||||
z: term()
|
||||
)
|
||||
)
|
||||
|
||||
assert return.(:map_update_with_unknown_keys, 2) ==
|
||||
dynamic(
|
||||
closed_map(
|
||||
__struct__: atom([A]),
|
||||
x: binary(),
|
||||
y: atom([nil]),
|
||||
z: term()
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "writes exports with inferred function types" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
def captured, do: &to_capture/1
|
||||
defp to_capture(<<"ok">>), do: :ok
|
||||
defp to_capture(<<"error">>), do: :error
|
||||
defp to_capture([_ | _]), do: :list
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
modules = compile_modules(files)
|
||||
exports = read_chunk(modules[A]).exports |> Map.new()
|
||||
|
||||
return = fn name, arity ->
|
||||
pair = {name, arity}
|
||||
%{^pair => %{sig: {:infer, nil, [{_, return}]}}} = exports
|
||||
return
|
||||
end
|
||||
|
||||
assert return.(:captured, 0)
|
||||
|> equal?(
|
||||
fun_from_non_overlapping_clauses([
|
||||
{[binary()], dynamic(atom([:ok, :error]))},
|
||||
{[non_empty_list(term(), term())], dynamic(atom([:list]))}
|
||||
])
|
||||
)
|
||||
end
|
||||
|
||||
test "writes exports for implementations" do
|
||||
files = %{
|
||||
"pi.ex" => """
|
||||
@@ -338,8 +230,8 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
previous clauses have already matched on the following types:
|
||||
|
||||
term(), integer()
|
||||
integer(), term()
|
||||
term(), integer()
|
||||
|
||||
│
|
||||
4 │ def foo(x, y) when is_integer(x) and is_integer(y), do: :three
|
||||
@@ -928,7 +820,7 @@ defmodule Module.Types.IntegrationTest do
|
||||
assert_no_warnings(files)
|
||||
end
|
||||
|
||||
test "redundant clause checking of mixed open and closed maps" do
|
||||
test "redundant clause checking of mixed open and closed maps (1)" do
|
||||
files = %{
|
||||
"mixed_open_closed_maps.ex" => """
|
||||
defmodule MixedOpenClosedMaps do
|
||||
@@ -983,6 +875,59 @@ defmodule Module.Types.IntegrationTest do
|
||||
assert_no_warnings(files)
|
||||
end
|
||||
|
||||
test "redundant clause checking of mixed open and closed maps (2)" do
|
||||
files = %{
|
||||
"mixed_open_closed_maps.ex" => """
|
||||
defmodule MixedOpenClose.Roles do
|
||||
defstruct engineering_admin: false,
|
||||
admin: false,
|
||||
support: false,
|
||||
service_desk: false,
|
||||
sales: false
|
||||
end
|
||||
|
||||
defmodule MixedOpenClose.User do
|
||||
defstruct roles: %MixedOpenClose.Roles{}
|
||||
end
|
||||
|
||||
defmodule MixedOpenClose.Policy do
|
||||
alias MixedOpenClose.{User, Roles}
|
||||
|
||||
@admin_actions [
|
||||
:access,
|
||||
:edit,
|
||||
:manage_roles,
|
||||
:global_search,
|
||||
:get_users_with_roles,
|
||||
:deactivate,
|
||||
:reactivate,
|
||||
:confirm_email,
|
||||
:soft_delete
|
||||
]
|
||||
|
||||
def can?(%User{roles: %Roles{support: true}}, _a, action)
|
||||
when action in [:global_search, :confirm_email, :get_users_with_roles], do: true
|
||||
|
||||
def can?(%User{roles: %Roles{service_desk: true}}, _a, action)
|
||||
when action in [:global_search, :confirm_email], do: true
|
||||
|
||||
def can?(%User{roles: %Roles{sales: true}}, _a, action)
|
||||
when action in [:edit, :confirm_email], do: true
|
||||
|
||||
def can?(%User{roles: roles}, _a, action)
|
||||
when action in @admin_actions and (roles.admin == true or roles.engineering_admin == true),
|
||||
do: true
|
||||
|
||||
def can?(user, %URI{} = uri, action), do: can?(user, uri, action)
|
||||
|
||||
def can?(_a, _b, _c), do: false
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
assert_no_warnings(files)
|
||||
end
|
||||
|
||||
test "redundant clause checking of open maps with distinct keys" do
|
||||
files = %{
|
||||
"large_head.ex" => """
|
||||
@@ -1874,7 +1819,7 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
defp read_chunk(binary) do
|
||||
assert {:ok, {_module, [{~c"ExCk", chunk}]}} = :beam_lib.chunks(binary, [~c"ExCk"])
|
||||
assert {:elixir_checker_v7, map} = :erlang.binary_to_term(chunk)
|
||||
assert {:elixir_checker_v8, map} = :erlang.binary_to_term(chunk)
|
||||
map
|
||||
end
|
||||
|
||||
|
||||
@@ -601,7 +601,10 @@ defmodule Module.Types.MapTest do
|
||||
describe "Map.pop_lazy/3" do
|
||||
test "checking" do
|
||||
assert typecheck!(Map.pop_lazy(%{key: 123}, :key, fn -> :error end)) ==
|
||||
dynamic(tuple([union(integer(), atom([:error])), empty_map()]))
|
||||
union(
|
||||
tuple([integer(), empty_map()]),
|
||||
dynamic(tuple([union(integer(), atom([:error])), empty_map()]))
|
||||
)
|
||||
|
||||
assert typecheck!([x], Map.pop_lazy(x, :key, fn -> :error end)) ==
|
||||
dynamic(tuple([term(), open_map(key: not_set())]))
|
||||
@@ -620,19 +623,21 @@ defmodule Module.Types.MapTest do
|
||||
])
|
||||
)
|
||||
|
||||
map = closed_map([{domain_key(:integer), atom([:before])}])
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
x = %{String.to_integer(x) => :before}
|
||||
Map.pop_lazy(x, 123, fn -> :after end)
|
||||
)
|
||||
) ==
|
||||
dynamic(
|
||||
tuple([
|
||||
atom([:before, :after]),
|
||||
closed_map([{domain_key(:integer), atom([:before])}])
|
||||
])
|
||||
)
|
||||
|> equal?(
|
||||
union(
|
||||
tuple([atom([:before]), map]),
|
||||
dynamic(tuple([atom([:before, :after]), map]))
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "inference" do
|
||||
@@ -833,6 +838,22 @@ defmodule Module.Types.MapTest do
|
||||
assert typeerror!([x = []], Map.put(x, :key, :value)) =~
|
||||
"incompatible types given to Map.put/3"
|
||||
end
|
||||
|
||||
test "errors with dynamic key and value" do
|
||||
assert typeerror!([key, value], Map.put(1, key, value)) |> strip_ansi() =~ """
|
||||
incompatible types given to Map.put/3:
|
||||
|
||||
Map.put(1, key, value)
|
||||
|
||||
given types:
|
||||
|
||||
integer(), dynamic(), dynamic()
|
||||
|
||||
but expected one of:
|
||||
|
||||
map(), term(), term()
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
describe "Map.put_new_lazy/3" do
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user