Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915d6dca16 | ||
|
|
a983c8c043 | ||
|
|
d33fd8e413 | ||
|
|
02603b147c | ||
|
|
e126dab368 | ||
|
|
38547285f1 | ||
|
|
4f07670d08 | ||
|
|
dcff0ffaea | ||
|
|
97407cde90 | ||
|
|
579fc97b97 | ||
|
|
003ef335e7 | ||
|
|
028cdbb91e | ||
|
|
794441a255 | ||
|
|
727106beb4 | ||
|
|
5e45871b5e | ||
|
|
0db308cbff | ||
|
|
35a7e3aa1f | ||
|
|
7340597020 | ||
|
|
3b6acc33d2 | ||
|
|
2ed048acc6 | ||
|
|
bf0c203d52 | ||
|
|
b074e6b159 | ||
|
|
bfe7315fbb | ||
|
|
28f4fdc21b | ||
|
|
ee1dc0729f | ||
|
|
32e724c87c | ||
|
|
7758084211 | ||
|
|
57d3098c2a | ||
|
|
427d21e410 | ||
|
|
24a188a016 | ||
|
|
457d52c89b | ||
|
|
6684dbd3f2 | ||
|
|
ccb2a742ad | ||
|
|
92094c9ef1 | ||
|
|
d4d5da2ccf | ||
|
|
666c082105 | ||
|
|
d2c9d39efe | ||
|
|
97d16038fe | ||
|
|
87cbf06ff1 | ||
|
|
caf39255a4 | ||
|
|
6440fb90a3 | ||
|
|
8793c68822 | ||
|
|
49ac038d67 | ||
|
|
3827b73d2d | ||
|
|
dc8a19028f | ||
|
|
357c346a9f | ||
|
|
6df9eb338c | ||
|
|
fa85b95250 | ||
|
|
a89655c434 | ||
|
|
730fd0f824 | ||
|
|
29d3b0cf3a | ||
|
|
ae94e6eb25 | ||
|
|
661e537cda | ||
|
|
4a5c076b7f | ||
|
|
297bc8302b | ||
|
|
f6413152ff | ||
|
|
c2edbaf173 | ||
|
|
ac9a659bc6 | ||
|
|
cc5837caa7 | ||
|
|
1738ed453f | ||
|
|
3e18aafa0a | ||
|
|
bcb73a468b | ||
|
|
613f631487 | ||
|
|
f08a84af10 | ||
|
|
b1a7afd061 | ||
|
|
e9bf5df25a | ||
|
|
2e7bb9b47f | ||
|
|
83c68cecc1 | ||
|
|
184b724483 | ||
|
|
47fea9580a | ||
|
|
6a6678f337 | ||
|
|
ec87c6b111 | ||
|
|
0c595489b6 | ||
|
|
cdbfa09edb | ||
|
|
baa25991fa | ||
|
|
a5c9121a7c | ||
|
|
177dc8ab59 | ||
|
|
bba8835ecc | ||
|
|
dea8456022 | ||
|
|
544a10ccfa | ||
|
|
ee14357bcd | ||
|
|
2fa136e60a | ||
|
|
f382affce3 | ||
|
|
36190985d2 | ||
|
|
957f84b7cd | ||
|
|
74fab3caf4 | ||
|
|
f1a6a4e019 | ||
|
|
c7884ca393 | ||
|
|
6f3fb272af | ||
|
|
8c14d3a818 | ||
|
|
9e10ea876f | ||
|
|
64f69cfee8 | ||
|
|
7844c98fe7 | ||
|
|
519eff5e29 | ||
|
|
e29c40a7e9 | ||
|
|
9fc3708980 | ||
|
|
c06b6a929e | ||
|
|
cfa3bb7429 | ||
|
|
195a4cf9f7 | ||
|
|
8daa2a6f67 | ||
|
|
d38f331db6 | ||
|
|
f642b30e42 | ||
|
|
23f5ade25c | ||
|
|
b0c1c35933 | ||
|
|
734d76a51b | ||
|
|
b2bf886884 | ||
|
|
d703e784f4 | ||
|
|
44fd817935 | ||
|
|
f8dcd941ff | ||
|
|
ce403643cd | ||
|
|
668c9a8f5d | ||
|
|
3175901d89 | ||
|
|
a2baac915a | ||
|
|
5a5e3855aa | ||
|
|
cd580f679c | ||
|
|
33347f107e | ||
|
|
cb15a3dd4c | ||
|
|
a4f98b1177 | ||
|
|
5b2f9c9201 | ||
|
|
b969aead71 | ||
|
|
bfbd7658b3 | ||
|
|
1327c0c5c3 | ||
|
|
64c61df2bf | ||
|
|
4363dffaf5 | ||
|
|
7be008c1b0 | ||
|
|
499a9c55d3 | ||
|
|
b18cdb0f7a | ||
|
|
62949b0728 | ||
|
|
cb108d5ce5 | ||
|
|
1f7ddd7acd | ||
|
|
7477c9326e | ||
|
|
c0e334df2a | ||
|
|
fd9dbf8490 | ||
|
|
7f45befed6 | ||
|
|
bbcd7f3094 | ||
|
|
51ac9bc324 | ||
|
|
c8f8d02cc0 | ||
|
|
d0634c9188 | ||
|
|
cc797de249 | ||
|
|
f56139aa2c | ||
|
|
8ac8230e18 | ||
|
|
0fca9beaf7 | ||
|
|
b3e3e8c6aa | ||
|
|
61603a8725 | ||
|
|
44c5069573 | ||
|
|
a96c04f260 | ||
|
|
d458fcb9f5 | ||
|
|
d507502ecd | ||
|
|
2bb27ea128 | ||
|
|
6fbc6e08a0 | ||
|
|
71bb017b61 | ||
|
|
1fadefe25f | ||
|
|
e1f34d09af | ||
|
|
ff21a9d601 | ||
|
|
1778bf211c | ||
|
|
99f2c8f2ea | ||
|
|
54369ba3e5 | ||
|
|
ba0f3935d3 | ||
|
|
b566edc2ab | ||
|
|
297f1f3edf | ||
|
|
764235f1da | ||
|
|
8d216b87cd | ||
|
|
2c7fa47ad6 | ||
|
|
71e1ddc64e | ||
|
|
f50f35fd1c | ||
|
|
5bc81aaa0a | ||
|
|
99cef0686c | ||
|
|
2308be941e | ||
|
|
79005e370e | ||
|
|
1f75ade71f | ||
|
|
a719cb3b21 | ||
|
|
ac901d9d27 | ||
|
|
f9966230ee | ||
|
|
9b729ca4c1 | ||
|
|
8cba20cb3e | ||
|
|
b7e0d657d1 | ||
|
|
2c3edbe1c7 | ||
|
|
fec9899ead | ||
|
|
f54b192823 | ||
|
|
855df4fc77 | ||
|
|
f87fbc2833 | ||
|
|
2b8ee38660 | ||
|
|
8d2775051d | ||
|
|
26855eda9f | ||
|
|
ea77a68daa | ||
|
|
cb49bfb6e4 | ||
|
|
c3ec400607 | ||
|
|
c03caa18cf | ||
|
|
bd68532c27 | ||
|
|
a244323815 | ||
|
|
fff2cf0d81 | ||
|
|
b878f37577 | ||
|
|
9619116cde | ||
|
|
248a71e2fc | ||
|
|
aa6964547d | ||
|
|
5c7687bf6f | ||
|
|
e73dd0c17d | ||
|
|
f4037a31ba | ||
|
|
a5b9c5b5db | ||
|
|
fcea4b4755 | ||
|
|
24e63ccc47 | ||
|
|
d8bb1c4009 | ||
|
|
fa2c96ffc3 | ||
|
|
db203167e2 | ||
|
|
a0ca37cb8f | ||
|
|
592f44e2a8 | ||
|
|
5ebe19e333 | ||
|
|
9e3e817e42 | ||
|
|
a450d12912 | ||
|
|
65dbdef7dc | ||
|
|
4f5746369b | ||
|
|
274bc56f53 | ||
|
|
7d3047981c | ||
|
|
4f2868e632 | ||
|
|
41151190e5 | ||
|
|
711008a456 | ||
|
|
7a28203ff5 | ||
|
|
f4bbf76d0b | ||
|
|
9fdb835f26 | ||
|
|
95f982e004 | ||
|
|
7e08594ca0 | ||
|
|
2096f4156d | ||
|
|
9d845cf031 | ||
|
|
d74dff2bcf | ||
|
|
59a1ad9138 | ||
|
|
342c724863 | ||
|
|
35818f1cdb | ||
|
|
502207c9f7 | ||
|
|
c3bc849550 | ||
|
|
f8de42a053 | ||
|
|
ce33663780 | ||
|
|
752ca8864d | ||
|
|
8588a8d473 | ||
|
|
2a9a4f2cab | ||
|
|
0847e4b41a | ||
|
|
7aafd6ca77 | ||
|
|
5ff421ca27 | ||
|
|
4425d684a0 | ||
|
|
e3f538cd25 |
@@ -3,7 +3,8 @@
|
||||
|
||||
---
|
||||
name: Report an issue
|
||||
description: Tell us about something that is not working the way we (probably) intend
|
||||
description:
|
||||
Tell us about something that is not working the way we (probably) intend
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -12,17 +13,11 @@ body:
|
||||
|
||||
|
||||
Please, do not use this form for guidance, questions or support.
|
||||
Try instead in [Elixir Forum](https://elixirforum.com) or any of
|
||||
our online communities (Slack, Discord, etc).
|
||||
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
attributes:
|
||||
label: Existing issue
|
||||
description: Please search [existing issues](https://github.com/elixir-lang/elixir/issues) before continuing.
|
||||
options:
|
||||
- label: I have searched existing issues and could not find a duplicate.
|
||||
required: true
|
||||
Try instead in [Elixir Forum](https://elixirforum.com),
|
||||
the [IRC Chat](https://web.libera.chat/#elixir),
|
||||
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
|
||||
[Slack](https://elixir-slackin.herokuapp.com),
|
||||
[Discord](https://discord.gg/elixir) or in other online communities.
|
||||
|
||||
- type: textarea
|
||||
id: elixir-and-otp-version
|
||||
|
||||
@@ -7,5 +7,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: CI for Markdown content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Run markdownlint
|
||||
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
|
||||
with:
|
||||
globs: |
|
||||
lib/elixir/pages/**/*.md
|
||||
README.md
|
||||
+83
-60
@@ -1,13 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
paths-ignore:
|
||||
- "lib/**/*.md"
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
@@ -19,64 +21,65 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "29.0"
|
||||
- otp_version: "28.1"
|
||||
deterministic: true
|
||||
- otp_version: "28.4"
|
||||
docs: true
|
||||
- otp_version: "28.1"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
coverage: true
|
||||
- otp_version: "28.1"
|
||||
otp_latest: true
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "27.3"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "27.0"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "26.0"
|
||||
- otp_version: master
|
||||
development: true
|
||||
- otp_version: maint
|
||||
development: true
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
# Earlier Erlang/OTP versions ignored compiler directives
|
||||
# when using warnings_as_errors. So we only set ERLC_OPTS
|
||||
# from Erlang/OTP 27+.
|
||||
env:
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
- name: Set ERL_COMPILER_OPTIONS
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
|
||||
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
make compile
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
continue-on-error: ${{ matrix.development == true }}
|
||||
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
- name: Elixir test suite
|
||||
run: make test_elixir
|
||||
continue-on-error: ${{ matrix.development == true }}
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
env:
|
||||
COVER: "${{ matrix.coverage }}"
|
||||
|
||||
- name: "Calculate Coverage"
|
||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
||||
if: "${{ matrix.coverage }}"
|
||||
- name: Build docs (ExDoc main)
|
||||
if: ${{ matrix.docs }}
|
||||
if: ${{ matrix.otp_latest }}
|
||||
run: |
|
||||
cd ..
|
||||
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
|
||||
@@ -85,67 +88,87 @@ jobs:
|
||||
cd ../elixir/
|
||||
git fetch --tags
|
||||
DOCS_OPTIONS="--warnings-as-errors" make docs
|
||||
|
||||
- name: "Calculate Coverage"
|
||||
if: ${{ matrix.coverage }}
|
||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Check reproducible builds
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: |
|
||||
rm -rf .git
|
||||
# Recompile System without .git
|
||||
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
|
||||
taskset 1 make check_reproducible
|
||||
- name: "Upload Coverage Artifact"
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: "${{ matrix.coverage }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: TestCoverage
|
||||
path: cover/*
|
||||
|
||||
- name: Check reproducible builds
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: taskset 1 make check_reproducible
|
||||
|
||||
- name: Check git is not required
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: |
|
||||
rm -rf .git
|
||||
cd lib/elixir
|
||||
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
|
||||
|
||||
test_windows:
|
||||
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
|
||||
runs-on: windows-2022
|
||||
|
||||
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version:
|
||||
- "29.0"
|
||||
- "28.1"
|
||||
- "27.3"
|
||||
|
||||
otp_version: ["26.2", "27.3", "28.1"]
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
Remove-Item -Recurse -Force '.git'
|
||||
make compile
|
||||
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
|
||||
- name: Elixir test suite
|
||||
run: |
|
||||
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
|
||||
make test_elixir
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
- name: Check POSIX-compliant
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
|
||||
license_compliance:
|
||||
name: Check Licence Compliance
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Use HTTPS instead of SSH for Git cloning
|
||||
id: git-config
|
||||
shell: bash
|
||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: "Run OSS Review Toolkit"
|
||||
id: ort
|
||||
uses: ./.github/workflows/ort
|
||||
with:
|
||||
upload-reports: true
|
||||
fail-on-violation: true
|
||||
report-formats: "WebApp"
|
||||
version: "${{ github.sha }}"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2026 The Elixir Team
|
||||
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "29 8 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
zizmor:
|
||||
name: Zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
@@ -1,41 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: License Compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
license_compliance:
|
||||
name: Check License Compliance
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Use HTTPS instead of SSH for Git cloning
|
||||
id: git-config
|
||||
shell: bash
|
||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
uses: ./.github/workflows/ort
|
||||
with:
|
||||
upload-reports: true
|
||||
fail-on-violation: true
|
||||
report-formats: "WebApp"
|
||||
version: "${{ github.sha }}"
|
||||
@@ -1,41 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Markdown Content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
paths: &paths-filter
|
||||
- "**/*.md"
|
||||
- .github/workflows/markdown.yml
|
||||
- .markdownlint-cli2.jsonc
|
||||
|
||||
pull_request:
|
||||
paths: *paths-filter
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Release Notifications
|
||||
name: Notify
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -15,20 +15,17 @@ jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
name: Notify
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
|
||||
- name: Run Elixir script
|
||||
env:
|
||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
||||
run: |
|
||||
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
|
||||
elixir .github/workflows/notify.exs ${{ github.ref_name }}
|
||||
@@ -46,7 +46,6 @@ runs:
|
||||
repository: oss-review-toolkit/ort-config
|
||||
ref: "main"
|
||||
path: ".ort-config"
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup ORT Config
|
||||
id: setup-ort-config
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
name: POSIX Compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: &paths-filter
|
||||
- .github/workflows/posix_compliance.yml
|
||||
- bin/elixir
|
||||
- bin/elixirc
|
||||
- bin/iex
|
||||
|
||||
pull_request:
|
||||
paths: *paths-filter
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
check_posix_compliance:
|
||||
name: Check POSIX compliance
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
|
||||
- name: Run ShellCheck on bin/ dir
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && \
|
||||
echo "bin/elixir is POSIX compliant"
|
||||
|
||||
shellcheck bin/elixirc && \
|
||||
echo "bin/elixirc is POSIX compliant"
|
||||
|
||||
shellcheck bin/iex && \
|
||||
echo "bin/iex is POSIX compliant"
|
||||
@@ -1,19 +1,16 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Releases
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*.*
|
||||
|
||||
tags:
|
||||
- v*
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
LANG: C.UTF-8
|
||||
@@ -23,68 +20,64 @@ permissions:
|
||||
|
||||
jobs:
|
||||
create_draft_release:
|
||||
name: Create draft release
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Create draft release
|
||||
if: github.ref_type != 'branch'
|
||||
run: |
|
||||
gh release create \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$GITHUB_REF_NAME" \
|
||||
--repo ${{ github.repository }} \
|
||||
--title ${{ github.ref_name }} \
|
||||
--notes '' \
|
||||
--draft \
|
||||
"$GITHUB_REF_NAME"
|
||||
${{ github.ref_name }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# zizmor: ignore[artipacked]
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
if: github.ref_type == 'branch'
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Update ${{ github.ref_name }}-latest
|
||||
if: github.ref_type == 'branch'
|
||||
run: |
|
||||
ref_name="${GITHUB_REF_NAME}-latest"
|
||||
ref_name=${{ github.ref_name }}-latest
|
||||
|
||||
if ! gh release view "$ref_name"; then
|
||||
if ! gh release view $ref_name; then
|
||||
gh release create \
|
||||
--latest=false \
|
||||
--title "$ref_name" \
|
||||
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
|
||||
"$ref_name"
|
||||
--title $ref_name \
|
||||
--notes "Automated release for latest ${{ github.ref_name }}." \
|
||||
$ref_name
|
||||
fi
|
||||
|
||||
git tag "$ref_name" --force
|
||||
git push origin "$ref_name" --force
|
||||
git tag $ref_name --force
|
||||
git push origin $ref_name --force
|
||||
|
||||
build:
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
name: "Build Elixir"
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- otp: 26
|
||||
otp_version: "26.0"
|
||||
- otp: 27
|
||||
otp_version: "27.0"
|
||||
|
||||
- otp: 28
|
||||
otp_version: "28.0"
|
||||
build_docs: build_docs
|
||||
|
||||
- otp: 29
|
||||
otp_version: "29.0"
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 50
|
||||
|
||||
- name: "Build Release"
|
||||
uses: ./.github/workflows/release_pre_built
|
||||
@@ -99,39 +92,38 @@ jobs:
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- name: "Upload linux release artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.zip
|
||||
|
||||
- name: "Upload Windows release artifacts"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- name: "Upload windows release artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.exe
|
||||
|
||||
- name: "Upload doc artifacts"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: matrix.build_docs
|
||||
with:
|
||||
name: Docs
|
||||
path: Docs.zip*
|
||||
|
||||
sign:
|
||||
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
|
||||
needs: [build]
|
||||
environment: release
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
otp: [27, 28, 29]
|
||||
otp: [26, 27, 28]
|
||||
flavor: [windows, linux]
|
||||
|
||||
env:
|
||||
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
|
||||
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -139,20 +131,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Download build"
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
|
||||
- name: Log in to Azure
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: "Sign files with Trusted Signing"
|
||||
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
@@ -181,15 +172,17 @@ jobs:
|
||||
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
|
||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- name: "Upload linux release artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
|
||||
sbom:
|
||||
name: Generate SBoM
|
||||
|
||||
needs: [build, sign]
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
@@ -205,13 +198,11 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: "Download Build Artifacts"
|
||||
id: download-build-artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -226,7 +217,7 @@ jobs:
|
||||
|
||||
- name: Attest Distribution Assets with SBoM
|
||||
id: attest-sbom
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
|
||||
with:
|
||||
subject-path: |
|
||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
||||
@@ -246,19 +237,15 @@ jobs:
|
||||
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
|
||||
done
|
||||
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
|
||||
env:
|
||||
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
|
||||
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
|
||||
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
|
||||
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
|
||||
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
||||
|
||||
- name: "Assemble Release SBoM Artifacts"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: "SBoM"
|
||||
path: |
|
||||
@@ -268,38 +255,37 @@ jobs:
|
||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
||||
|
||||
- name: "Assemble Distribution Attestations"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: "Attestations"
|
||||
path: "attestations/*.sigstore"
|
||||
|
||||
upload-release:
|
||||
name: Upload release
|
||||
needs: [create_draft_release, build, sign, sbom]
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload Pre-build
|
||||
- name: Upload Pre-built
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
|
||||
tag="${GITHUB_REF_NAME}-latest"
|
||||
if [ "${{ github.ref_type }}" == "branch" ]; then
|
||||
tag=${{ github.ref_name }}-latest
|
||||
else
|
||||
tag="$GITHUB_REF_NAME"
|
||||
tag="${{ github.ref_name }}"
|
||||
fi
|
||||
|
||||
gh release upload \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--repo ${{ github.repository }} \
|
||||
--clobber \
|
||||
"$tag" \
|
||||
elixir-otp-*.zip \
|
||||
@@ -314,18 +300,15 @@ jobs:
|
||||
bom.*
|
||||
|
||||
upload-builds-hex-pm:
|
||||
name: Upload builds to hex.pm
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [build, sign]
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency: builds-hex-pm
|
||||
environment: release
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
|
||||
|
||||
steps:
|
||||
- name: "Check if variables are set up"
|
||||
if: "${{ ! vars.HEX_AWS_REGION }}"
|
||||
@@ -333,7 +316,7 @@ jobs:
|
||||
echo "Required variables for uploading to hex.pm are not set up, skipping..."
|
||||
exit 1
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -344,10 +327,12 @@ jobs:
|
||||
|
||||
- name: Upload Precompiled to S3
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
||||
|
||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
||||
dest=${zip/elixir/${GITHUB_REF_NAME}}
|
||||
dest=${zip/elixir/${ref_name}}
|
||||
surrogate_key=${dest/.zip$/}
|
||||
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
|
||||
@@ -356,16 +341,16 @@ jobs:
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${ref_name} >> purge_keys.txt
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Docs to S3
|
||||
run: |
|
||||
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
|
||||
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||
|
||||
unzip Docs.zip
|
||||
|
||||
@@ -386,7 +371,7 @@ jobs:
|
||||
- name: Update builds txt
|
||||
run: |
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name="$GITHUB_REF_NAME"
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
||||
|
||||
@@ -439,7 +424,6 @@ jobs:
|
||||
for key in $(cat purge_keys.txt); do
|
||||
purge "${key}"
|
||||
done
|
||||
|
||||
env:
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
|
||||
@@ -1,58 +1,45 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Release Pre-build
|
||||
description: "Builds Elixir release, ExDoc and generates docs"
|
||||
|
||||
name: "Release pre built"
|
||||
description: "Builds elixir release, ExDoc and generates docs"
|
||||
inputs:
|
||||
otp:
|
||||
description: "The major OTP version"
|
||||
|
||||
otp_version:
|
||||
description: "The exact OTP version (major.minor[.patch])"
|
||||
|
||||
build_docs:
|
||||
description: "Whether docs have to be built"
|
||||
|
||||
description: "If docs have to be built or not"
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
|
||||
with:
|
||||
otp-version: ${{ inputs.otp_version }}
|
||||
version-type: strict
|
||||
|
||||
- name: Build Elixir Release
|
||||
shell: bash
|
||||
run: | # zizmor: ignore[github-env]
|
||||
run: |
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
env:
|
||||
INPUT_OTP: ${{ inputs.otp }}
|
||||
|
||||
- name: Install NSIS
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y nsis
|
||||
|
||||
- name: Build Elixir Windows Installer
|
||||
shell: bash
|
||||
run: |
|
||||
export OTP_VERSION="$INPUT_OTP_VERSION"
|
||||
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
|
||||
export OTP_VERSION=${{ inputs.otp_version }}
|
||||
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
|
||||
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
||||
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
|
||||
env:
|
||||
INPUT_OTP: ${{ inputs.otp }}
|
||||
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
|
||||
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||
- name: Get ExDoc ref
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: | # zizmor: ignore[github-env]
|
||||
if [ "$GITHUB_REF_NAME" = "main" ]; then
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
ref=main
|
||||
else
|
||||
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
@@ -64,7 +51,6 @@ runs:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: ${{ env.EX_DOC_REF }}
|
||||
path: ex_doc
|
||||
persist-credentials: false
|
||||
- name: Build ex_doc
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
|
||||
+2
-3
@@ -10,10 +10,9 @@
|
||||
/lib/elixir/test/ebin/
|
||||
/man/elixir.1
|
||||
/man/iex.1
|
||||
/Docs.zip
|
||||
/Precompiled.zip
|
||||
/Docs-v*.zip
|
||||
/Precompiled-v*.zip
|
||||
/.eunit
|
||||
.elixir.plt
|
||||
erl_crash.dump
|
||||
/cover/
|
||||
.tool-versions
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
{
|
||||
"globs": [
|
||||
"**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".git/**"
|
||||
],
|
||||
"gitignore": true,
|
||||
"config": {
|
||||
// Consecutive header levels (h1 -> h2 -> h3).
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content.
|
||||
"MD024": {
|
||||
// Duplication is allowed for headings with different parents.
|
||||
"siblings_only": true
|
||||
},
|
||||
// Trailing punctuation in heading.
|
||||
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
|
||||
// the default values.
|
||||
"MD026": {
|
||||
"punctuation": ".,;:。,;:!"
|
||||
},
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"h1",
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"picture",
|
||||
"source",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false,
|
||||
// Our tables are too large to align.
|
||||
"MD060": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
{
|
||||
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content. That's fine.
|
||||
"MD024": false,
|
||||
// Some headers finish with ! because it refers to a function name
|
||||
"MD026": false,
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"h1",
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"picture",
|
||||
"source",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false
|
||||
}
|
||||
+438
-355
@@ -4,428 +4,511 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.20
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
# Changelog for Elixir v1.19
|
||||
|
||||
## Type system improvements
|
||||
|
||||
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
|
||||
### Type checking of protocol dispatch and implementations
|
||||
|
||||
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
|
||||
This release also adds type checking when dispatching and implementing protocols.
|
||||
|
||||
### Type inference of guards
|
||||
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
|
||||
|
||||
This release also performs inference of guards! Let's see some examples:
|
||||
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
|
||||
|
||||
```elixir
|
||||
def example(x, y) when is_list(x) and is_integer(y)
|
||||
```
|
||||
|
||||
The code above correctly infers `x` is a list and `y` is an integer.
|
||||
|
||||
```elixir
|
||||
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
|
||||
```
|
||||
|
||||
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
|
||||
|
||||
```elixir
|
||||
def example(x) when is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
|
||||
|
||||
```elixir
|
||||
def example(x) when not is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
|
||||
|
||||
You can also have expressions that assert on the size of data structures:
|
||||
|
||||
```elixir
|
||||
def example(x) when tuple_size(x) < 3
|
||||
```
|
||||
|
||||
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
|
||||
|
||||
### Whole-body type inference
|
||||
|
||||
Elixir also performs inference based on the function body itself. Take the following code:
|
||||
|
||||
```elixir
|
||||
def add_foo_and_bar(data) do
|
||||
data.foo + data.bar
|
||||
end
|
||||
```
|
||||
|
||||
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
|
||||
|
||||
Here is another example:
|
||||
|
||||
```elixir
|
||||
def sum_to_string(a, b) do
|
||||
Integer.to_string(a + b)
|
||||
end
|
||||
```
|
||||
|
||||
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
|
||||
|
||||
### Typing across clauses
|
||||
|
||||
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
|
||||
|
||||
```elixir
|
||||
case System.get_env("SOME_VAR") do
|
||||
nil -> :not_found
|
||||
value -> {:ok, String.upcase(value)}
|
||||
end
|
||||
```
|
||||
|
||||
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
|
||||
|
||||
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
|
||||
|
||||
### Typing of atom and domain keys in maps
|
||||
|
||||
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
|
||||
|
||||
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
|
||||
|
||||
```elixir
|
||||
%{123 => "hello", 456.0 => :ok}
|
||||
```
|
||||
|
||||
will have the type:
|
||||
|
||||
```elixir
|
||||
%{integer() => binary(), float() => :ok}
|
||||
```
|
||||
|
||||
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
|
||||
|
||||
```elixir
|
||||
%{integer() => integer(), root: integer()}
|
||||
```
|
||||
|
||||
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
|
||||
|
||||
### Typing of map operations
|
||||
|
||||
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
|
||||
|
||||
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
|
||||
|
||||
```elixir
|
||||
Map.put(map, :key, 123)
|
||||
#=> returns type %{..., key: integer()}
|
||||
|
||||
Map.delete(map, :key)
|
||||
#=> returns type %{..., key: not_set()}
|
||||
```
|
||||
|
||||
As you can see, we track when keys are set and also when they are removed.
|
||||
|
||||
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
|
||||
|
||||
```elixir
|
||||
Map.replace(map, :key, 123)
|
||||
#=> returns type %{..., key: if_set(integer())}
|
||||
```
|
||||
|
||||
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
|
||||
|
||||
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
def name(map), do: Map.fetch!(map, :name)
|
||||
end
|
||||
|
||||
defmodule CallsUser do
|
||||
def calls_name do
|
||||
User.name(%{})
|
||||
defmodule Example do
|
||||
def my_code(first..last//step = range) do
|
||||
"hello #{range}"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The code above has a type violation, which is now caught by the type system:
|
||||
the above emits the following warnings:
|
||||
|
||||
```text
|
||||
warning: incompatible types given to User.name/1:
|
||||
```
|
||||
warning: incompatible value given to string interpolation:
|
||||
|
||||
User.name(%{})
|
||||
data
|
||||
|
||||
it has type:
|
||||
|
||||
%Range{first: term(), last: term(), step: term()}
|
||||
|
||||
but expected a type that implements the String.Chars protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
|
||||
%Version.Requirement{}
|
||||
) or atom() or binary() or float() or integer() or list(term())
|
||||
```
|
||||
|
||||
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def my_code(%Date{} = date) do
|
||||
for(x <- date, do: x)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
will emit:
|
||||
|
||||
```
|
||||
warning: incompatible value given to for-comprehension:
|
||||
|
||||
x <- date
|
||||
|
||||
it has type:
|
||||
|
||||
%Date{year: term(), month: term(), day: term(), calendar: term()}
|
||||
|
||||
but expected a type that implements the Enumerable protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
|
||||
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
|
||||
) or fun() or list(term()) or non_struct_map()
|
||||
```
|
||||
|
||||
### Type checking and inference of anonymous functions
|
||||
|
||||
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def run do
|
||||
fun = fn %{} -> :map end
|
||||
fun.("hello")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
|
||||
|
||||
```
|
||||
warning: incompatible types given on function application:
|
||||
|
||||
fun.("hello")
|
||||
|
||||
given types:
|
||||
|
||||
%{name: not_set()}
|
||||
binary()
|
||||
|
||||
but expected one of:
|
||||
but function has type:
|
||||
|
||||
dynamic(%{..., name: term()})
|
||||
(dynamic(map()) -> :map)
|
||||
|
||||
type warning found at:
|
||||
typing violation found at:
|
||||
│
|
||||
16 │ User.name(%{})
|
||||
│ ~
|
||||
6 │ fun.("hello")
|
||||
│ ~
|
||||
│
|
||||
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
|
||||
└─ mod.exs:6:8: Example.run/0
|
||||
```
|
||||
|
||||
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
|
||||
|
||||
## Compile-time improvements
|
||||
## Faster compile times in large projects
|
||||
|
||||
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
|
||||
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
|
||||
|
||||
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
|
||||
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU cores.
|
||||
|
||||
* Errors during compilation may have less precise stacktraces
|
||||
### Code loading bottlenecks
|
||||
|
||||
* Anonymous functions within `defmodule` can have only up to 20 arguments.
|
||||
If this is an issue, you can use maps or tuples to group the data.
|
||||
Note the functions themselves inside `defmodule`, such as the ones defined
|
||||
inside `def` and friends, can still have up to 255 arguments
|
||||
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing parallelization, especially on large projects.
|
||||
|
||||
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
|
||||
This release makes it so modules are loaded lazily. This reduces the pressure on the code server and the amount of work during compilation, with reports of more than two times faster compilation for large projects. The benefits depend on the codebase size and the number of CPU cores available.
|
||||
|
||||
## v1.20.4 (2026-08-28)
|
||||
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
|
||||
|
||||
There are two potential regressions with this approach. The first one happens if you spawn processes during compilation which invoke other modules defined within the same project. For example:
|
||||
|
||||
```elixir
|
||||
defmodule MyLib.SomeModule do
|
||||
list = [...]
|
||||
|
||||
Task.async_stream(list, fn item ->
|
||||
MyLib.SomeOtherModule.do_something(item)
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
Because the spawned process is not visible to the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_compiled!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
|
||||
|
||||
The second one is related to `@on_load` callbacks (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invoke other modules defined within the same project. For example:
|
||||
|
||||
```elixir
|
||||
defmodule MyLib.SomeModule do
|
||||
@on_load :init
|
||||
|
||||
def init do
|
||||
MyLib.AnotherModule.do_something()
|
||||
end
|
||||
|
||||
def something_else do
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
MyLib.SomeModule.something_else()
|
||||
```
|
||||
|
||||
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
|
||||
|
||||
Both snippets above could actually lead to non-deterministic compilation failures in the past, and as a result of these changes, compiling these cases are now deterministic.
|
||||
|
||||
### Parallel compilation of dependencies
|
||||
|
||||
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
|
||||
|
||||
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, as outlined in the previous section, there were pathological cases where performance gains would be left on the table, such as when compiling dependencies with native code or dependencies where one or two large files would take most of the compilation time.
|
||||
|
||||
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
|
||||
|
||||
## Improved pretty printing algorithm
|
||||
|
||||
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
[1, 2, ...],
|
||||
[1, ...],
|
||||
[...],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
|
||||
|
||||
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
|
||||
|
||||
## Erlang/OTP 28 support
|
||||
|
||||
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped into abstract syntax trees by defining a `__escape__/1` callback.
|
||||
|
||||
On the other hand, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
|
||||
|
||||
```elixir
|
||||
defmodule Foo do
|
||||
defstruct regex: ~r/foo/
|
||||
end
|
||||
```
|
||||
|
||||
You must do this:
|
||||
|
||||
```elixir
|
||||
defmodule Foo do
|
||||
defstruct [:regex]
|
||||
|
||||
def new do
|
||||
%Foo{regex: ~r/foo/}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## OpenChain certification
|
||||
|
||||
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
|
||||
|
||||
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
|
||||
* Each release is attested along with the Source SBoM.
|
||||
|
||||
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
|
||||
|
||||
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
|
||||
|
||||
## v1.19.6 (2026-08-28)
|
||||
|
||||
### 1. Security
|
||||
|
||||
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Base] Fix false positives in Base64 validation
|
||||
* [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring
|
||||
* [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression
|
||||
* [String] Fix final sigma handling in Greek casing
|
||||
* [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count
|
||||
|
||||
## v1.20.3 (2026-08-04)
|
||||
## v1.19.5 (2026-01-09)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel.ParallelCompiler] Batch type checker cache operations to improve compilation times
|
||||
* [Protocol] Optimize protocol consolidation to no longer load structs
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Improve type precision of the `++/2` operator
|
||||
* [Kernel] Improve type precision of the `send/2` return type
|
||||
* [Kernel] Improve type refinement for `and/2` and `or/2`
|
||||
* [Kernel] Fix type checking of `Map.put/3` on empty map types
|
||||
* [Kernel] Fix type checking of tuple insertion on equivalent tuple types
|
||||
* [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts
|
||||
* [Kernel] Fix variable refinements leaking from invalid `receive/after` timeout expressions
|
||||
* [Kernel] Fix rendering inferred types containing unions of static and dynamic functions
|
||||
* [Kernel] Ensure typing errors in `:maps.values/1` reference the correct function
|
||||
* [Kernel] Mark `%_{}` patterns as precise so subsequent redundant struct clauses are detected
|
||||
* [elixirc] Fix parsing of the `--profile time` option
|
||||
* [Kernel] Fix unnecessary recompilation when `dbg_callback` is modified at runtime
|
||||
* [Kernel] Fix parser crash on missing parentheses on expression following operator `not in`
|
||||
* [Kernel] Support fetching abstract code for modules compiled with Elixir v1.14 and earlier
|
||||
* [Protocol] Ensure protocol consolidation no longer stores outdated struct types. As a consequence, protocols types only track struct names at the moment
|
||||
* [Stream] Revert optimization which caused nested streams in `Stream.flat_map/2` to crash
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Fix `break!/1` with unknown expressions
|
||||
|
||||
## v1.20.2 (2026-06-23)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
|
||||
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
|
||||
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
|
||||
* [Module] Fix type checking when applying an empty function type
|
||||
* [Module] Fix type checking of bitstring patterns that reuse variables
|
||||
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
|
||||
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
|
||||
* [Module] Fix typing of list types involving dynamic or empty lists
|
||||
* [Module] Include bitstrings as a possible domain key in the type system
|
||||
* [Module] Preserve file metadata from each clause in type system warnings
|
||||
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
|
||||
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
|
||||
* [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
|
||||
|
||||
## v1.20.1 (2026-06-09)
|
||||
|
||||
### 1. Security
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters
|
||||
* [Code] Ensure `Code.require_file` releases the file if compilation fails
|
||||
* [Kernel] Fix documentation generation to use the correct version in search
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix archive.install] Validate paths and files when extracting archives
|
||||
* [mix format] Honor `--no-compile` option when loading plugins
|
||||
|
||||
## v1.20.0 (2026-06-03)
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Optimize compiler by flattening expr list only once
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Base] Optimize Base validation functions by using SWAR techniques
|
||||
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
|
||||
* [Code] Add `:dbg_callback` option to eval functions
|
||||
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
|
||||
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
|
||||
* [Enum] Add `Enum.min_max` sorter
|
||||
* [File] Add support for `[:raw]` opts in `File.read/2`
|
||||
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
|
||||
* [Float] Optimize `Float.round/2` by avoiding big integers
|
||||
* [Inspect] Increase inspect limit to help print deeply nested data structures
|
||||
* [Inspect] Support printing Erlang records (using Erlang notation)
|
||||
* [Integer] Add `Integer.ceil_div/2`
|
||||
* [Integer] Add `Integer.popcount/1`
|
||||
* [IO] Add `IO.iodata_empty?/1`
|
||||
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
|
||||
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
|
||||
* [Kernel] Detect and warn on redundant clauses
|
||||
* [Kernel] Perform type inference across applications
|
||||
* [Kernel] Print intermediate results of `dbg` for pipes
|
||||
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
|
||||
* [Kernel] Warn on unused requires
|
||||
* [List] Add `List.first!/1` and `List.last!/1`
|
||||
* [Module] Purge and delete modules if `after_compile/2` callback fails
|
||||
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
|
||||
* [Process] Add `Process.get_label/1`
|
||||
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
|
||||
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
|
||||
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
|
||||
* Add Software Bill of Materials guide to the Documentation
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Optimize autocompleting modules
|
||||
* [IEx.Helpers] Add `source/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix app.tree] Support `--output` option
|
||||
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
|
||||
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
|
||||
* [mix deps] Support filtering `mix deps` output
|
||||
* [mix deps.tree] Support `--output` option
|
||||
* [mix format] Support `--no-compile` option
|
||||
* [mix help] Support printing docs for types and callbacks
|
||||
* [mix source] Add `mix source MODULE` to print or open a given module/function location
|
||||
* [mix test] Add `mix test --dry-run`
|
||||
|
||||
### 2. Potential breaking changes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
|
||||
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
|
||||
|
||||
### 3. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
|
||||
* [File] Allowing preserving directory permissions in `File.cp_r/3`
|
||||
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
|
||||
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
|
||||
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
|
||||
* [File] Warn when defining `@type record()` for Erlang/OTP 29
|
||||
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
|
||||
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
|
||||
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
|
||||
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
|
||||
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
|
||||
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
|
||||
* [Kernel] Reject negative Duration in `to_timeout/1`
|
||||
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
|
||||
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
|
||||
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
|
||||
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
|
||||
* [String] Support empty pattern list in `String.count/2`
|
||||
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Ensure pry works across remote nodes
|
||||
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
|
||||
* [IEx] Fix usage of `#iex:break` as part of multi-line prompts
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Persist log level to app env in `Logger.configure/1`
|
||||
* [Logger.Backends] Do not crash on invalid metadata
|
||||
|
||||
## v1.19.4 (2025-11-27)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
|
||||
* [mix compile] Add a build lock around protocol consolidation in umbrellas
|
||||
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
|
||||
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
|
||||
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
|
||||
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
|
||||
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
|
||||
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
|
||||
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
|
||||
* [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Fix formatter crash when diffing takes too long
|
||||
* [ExUnit] Ensure parallel matches in `assert` propagate type information
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters)
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled
|
||||
* [mix escript.build] Ensure the `hex` application can be included in escripts
|
||||
|
||||
## v1.19.3 (2025-11-13)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Support /E modifier for regular expressions in config files
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Allow forcing specific compilers, such as `--force-elixir`, `--force-app`, etc
|
||||
* [mix help app:APP] Support showing helps for apps in Elixir and Erlang standard libraries
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [IO] Fix dialyzer warning on `IO.inspect :label`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Case] Fix crash when formatting errors caused by a linked/trapped exit during `setup_all`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.app] Ensure functions in the format `&Mod.fun/arity` can be written to .app files
|
||||
* [mix compile.app] Ensure strings with Unicode characters can be written to .app files
|
||||
|
||||
## v1.19.2 (2025-11-02)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Measure and optimize writing of `.beam` files in the compiler
|
||||
* [Kernel] Optimize rare scenarios where type checking took too long
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Add flag `--no-check-cwd` to skip compiler check to aid debugging
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [IO] Fix dialyzer warning on `IO.inspect :label`
|
||||
* [Kernel] Ensure we warn on deprecated `~~~` unary operator
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Reset ansi escapes before newlines in Logger
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Warn if `elixirc_paths` is not a list of string paths
|
||||
* [mix compile] Address regression where umbrella children were compiled too early and without respecting compilation flags
|
||||
* [mix deps.compile] Improve reliability of `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` across `mix escript.install`, `mix archive.install`, and others
|
||||
|
||||
## v1.19.1 (2025-10-20)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Address Dialyzer warnings when invoking `EEx.compile_string`
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Optimize how types are computed for pretty printing
|
||||
* [Kernel] Optimize how differences are computed in the type system
|
||||
* [Macro] Do not escape options given to `dbg/2`
|
||||
* [Protocol] Improve protocol violation warnings
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Do not attempt to touch deleted files when compilation fails and then resumed with missing files
|
||||
* [mix deps.compile] Do not spawn partitions when all dependencies are local and already compiled
|
||||
|
||||
## v1.19.0 (2025-10-16)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
|
||||
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
|
||||
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
|
||||
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
|
||||
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
|
||||
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
|
||||
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
|
||||
* [Code.Fragment] Add `Code.Fragment.lines/1`
|
||||
* [Enum] Provide more information on `Enum.OutOfBoundsError`
|
||||
* [Inspect] Allow `optional: :all` when deriving Inspect
|
||||
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
|
||||
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
|
||||
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
|
||||
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
|
||||
* [Kernel] Support `min/2` and `max/2` as guards
|
||||
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
|
||||
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
|
||||
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
|
||||
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
|
||||
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
|
||||
* [OptionParser] Support the `:regex` type
|
||||
* [OptionParser] Enhance parsing error to display available options
|
||||
* [Protocol] Type checking of protocols dispatch and implementations
|
||||
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
|
||||
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
|
||||
* [Registry] Add key-based partitioning of duplicate registries
|
||||
* [String] Add `String.count/2` to count occurrences of a pattern
|
||||
* [String] Update to Unicode 17.0.0
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Set a process label for each test
|
||||
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
|
||||
* [ExUnit.Case] Add `:test_group` to the test context
|
||||
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
|
||||
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
|
||||
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Accept any enumerable in `Logger.metadata/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
|
||||
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
|
||||
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
|
||||
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
|
||||
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun`, `mix help Mod.fun/arity`, and `mix help app:package`
|
||||
* [mix format] Add options to mix format to allow excluding of files
|
||||
* [mix test] Add `--name-pattern` option to `mix test`
|
||||
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
|
||||
* [mix xref graph] Add support for `--format json`
|
||||
* [mix xref graph] Emit a warning if `--source` is part of a cycle
|
||||
* [Mix] Support the `:compilers` option
|
||||
* [Mix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
|
||||
* [Code] Properly handle column annotation for `in` in `not in` expressions
|
||||
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
|
||||
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
|
||||
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
|
||||
* [Inspect] Inspect ill-formed structs as maps
|
||||
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
|
||||
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
|
||||
* [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
|
||||
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
|
||||
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
|
||||
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Fix order of pinned variables in failure reports
|
||||
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
|
||||
* [ExUnit.Case] Do not crash on empty test unit groups
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Abort pipelines when there is an error in any step along the way
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix cmd] Preserve argument quoting in subcommands by no longer performing shell expansion. To revert to the previous behaviour, pass `--shell` before the command name
|
||||
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
|
||||
* [mix compile] Avoid failures when locking compilation across different users
|
||||
* [mix compile] Fix race condition when renaming files used by the compilation lock
|
||||
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
|
||||
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
|
||||
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
|
||||
* [mix xref graph] Provide more consistent output by considering strong connected components when computing graphs. This means that, if you were using `mix xref graph --format cycles`, you will now get fewer but larger cycles, as cycle between `A` and `B` and another between `A` and `C` is now considered a single cycle between `A`, `B`, and `C`. Note the cycles themselves are not problematic, unless they have compile-time dependencies in them
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
|
||||
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
|
||||
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and the task itself has no effect
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
|
||||
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
|
||||
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
|
||||
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
|
||||
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
|
||||
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}`, now requires the given variable (or expression) to explicitly pattern match on the struct before it can be updated. This is because, thanks to the type system, pattern matching on structs can find more errors, more reliably, and we want to promote its usage. Once pattern matching is added, you may optionally convert the struct update syntax into the map update syntax `%{uri | path: "/foo/bar"}` with no less of typing guarantees
|
||||
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
|
||||
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
|
||||
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
|
||||
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
|
||||
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
|
||||
|
||||
## v1.19
|
||||
## v1.18
|
||||
|
||||
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
|
||||
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
Contact: <elixir-lang-conduct@googlegroups.com>
|
||||
Contact: elixir-lang-conduct@googlegroups.com
|
||||
|
||||
## Why have a Code of Conduct?
|
||||
|
||||
@@ -51,15 +51,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
|
||||
|
||||
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
|
||||
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
|
||||
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
|
||||
|
||||
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
|
||||
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
|
||||
|
||||
|
||||
+45
-96
@@ -78,7 +78,6 @@ introduced behavior, especially for bug fixes and major changes:
|
||||
*fails* before your change and *passes* afterward. This makes it easier to
|
||||
confirm that the fix addresses the underlying issue and helps prevent
|
||||
regressions in the future.
|
||||
|
||||
* **New Features or Major Changes:** If you are adding a new feature or making
|
||||
major changes to existing functionality, please add tests that cover the
|
||||
major parts of that functionality. Aim to have the best code coverage possible.
|
||||
@@ -89,9 +88,7 @@ We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
|
||||
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
|
||||
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
|
||||
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
|
||||
## Reviewing changes
|
||||
@@ -99,11 +96,15 @@ 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 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.
|
||||
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.
|
||||
|
||||
The Elixir team may optionally assign someone to review a pull request.
|
||||
If someone is assigned, they must explicitly approve the code before
|
||||
@@ -114,63 +115,36 @@ into the repository. If you have carefully organized your commits and
|
||||
believe they should be merged without squashing, please mention it in
|
||||
a comment.
|
||||
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
|
||||
```sh
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
|
||||
# and generate HTML and EPUB documents:
|
||||
make docs
|
||||
```
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
|
||||
## Licensing and Compliance Requirements
|
||||
|
||||
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
|
||||
guidelines on licensing and compliance. Below is a summary of the key points
|
||||
affecting **all external contributors**:
|
||||
|
||||
* Accepted Licenses: Any code contributed must be licensed under the
|
||||
`Apache-2.0` license.
|
||||
|
||||
* SPDX License Headers: With the exception of approved test fixture files,
|
||||
all new or modified files in a pull request must include correct SPDX
|
||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
||||
instance, please use:
|
||||
|
||||
- Accepted Licenses: Any code contributed must be licensed under the
|
||||
`Apache-2.0` license.
|
||||
- SPDX License Headers: With the exception of approved test fixture files,
|
||||
all new or modified files in a pull request must include correct SPDX
|
||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
||||
instance, please use:
|
||||
|
||||
```elixir
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
```
|
||||
|
||||
- No Executable Binaries: Contributions must **not** include any executable
|
||||
binary files. If you require an exception (for example, certain test artifacts),
|
||||
please see the policy on how to request approval and document exceptions.
|
||||
- Preserving Copyright and License Info: If you copy code from elsewhere,
|
||||
ensure that **all original copyright and license notices remain intact**. If
|
||||
they are missing or incomplete, you must add them.
|
||||
- Failure to Comply: Pull requests that do not meet these licensing and
|
||||
compliance standards will be rejected or require modifications before merging.
|
||||
- Developer Certificate of Origin: All contributions are subject to the
|
||||
Developer Certificate of Origin.
|
||||
|
||||
* No Executable Binaries: Contributions must **not** include any executable
|
||||
binary files. If you require an exception (for example, certain test artifacts),
|
||||
please see the policy on how to request approval and document exceptions.
|
||||
|
||||
* Preserving Copyright and License Info: If you copy code from elsewhere,
|
||||
ensure that **all original copyright and license notices remain intact**. If
|
||||
they are missing or incomplete, you must add them.
|
||||
|
||||
* Failure to Comply: Pull requests that do not meet these licensing and
|
||||
compliance standards will be rejected or require modifications before merging.
|
||||
|
||||
* Developer Certificate of Origin: All contributions are subject to the
|
||||
Developer Certificate of Origin.
|
||||
|
||||
```text
|
||||
```
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
@@ -197,52 +171,27 @@ affecting **all external contributors**:
|
||||
involved.
|
||||
```
|
||||
|
||||
See <https://developercertificate.org/> for a copy of the Developer Certificate
|
||||
See http://developercertificate.org/ for a copy of the Developer Certificate
|
||||
of Origin license.
|
||||
|
||||
## Using AI and coding agents
|
||||
## Building documentation
|
||||
|
||||
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.
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir:
|
||||
|
||||
That said, here are examples of how one might (or might not) use AI and coding
|
||||
agents in Elixir spaces:
|
||||
```sh
|
||||
# After cloning and compiling Elixir, in its parent directory:
|
||||
git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
|
||||
```
|
||||
|
||||
* 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 go back to Elixir's root directory and run:
|
||||
|
||||
* Do not use coding agents to tackle existing issues unless they have the
|
||||
"Contributions Welcome" label.
|
||||
```sh
|
||||
make docs # to generate HTML pages
|
||||
make docs DOCS_FORMAT=epub # to generate EPUB documents
|
||||
```
|
||||
|
||||
* If you request a feature on the mailing list and it is accepted, you may
|
||||
use coding agents to implement it, as long as it follows the AI Contributions
|
||||
guidelines below.
|
||||
|
||||
* When automating AI usage on the Elixir codebase for performance improvements
|
||||
or security fixes, pair it with a separate set of agents whose job is to argue
|
||||
against and try to invalidate any proposed change. And treat their approval as
|
||||
advisory: a human must still validate it before opening issues or pull requests.
|
||||
|
||||
If any code is written by AI, then you must follow the guidelines below.
|
||||
|
||||
### AI contributions
|
||||
|
||||
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
|
||||
Certificate of Origin (DCO). The human submitter is responsible for:
|
||||
|
||||
* Reviewing all AI-generated code
|
||||
* Ensuring compliance with licensing requirements
|
||||
* Adding their own Signed-off-by tag to certify the DCO
|
||||
* Taking full responsibility for the contribution
|
||||
* Disclosing use of AI for comments and code contributions
|
||||
|
||||
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
|
||||
AI in the development process. Contributions should include an Assisted-by tag in the
|
||||
following format:
|
||||
|
||||
Assisted-by: AGENT_NAME:MODEL_VERSION
|
||||
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).
|
||||
|
||||
@@ -32,9 +32,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||
#==> Functions
|
||||
|
||||
define CHECK_ERLANG_RELEASE
|
||||
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 27)])' -s erlang halt | grep -q '^true'; \
|
||||
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 26)])' -s erlang halt | grep -q '^true'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \
|
||||
echo "At least Erlang/OTP 26.0 is required to build Elixir"; \
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
||||
@@ -107,10 +107,8 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
|
||||
fi
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
|
||||
|
||||
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
|
||||
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
|
||||
unicode: $(UNICODE)
|
||||
|
||||
+13
-10
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
|
||||
## 2. Scope
|
||||
|
||||
This policy applies to the Elixir Programming language, located at
|
||||
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
|
||||
https://github.com/elixir-lang/elixir. It covers every file, and contribution
|
||||
made, including documentation and any associated assets.
|
||||
|
||||
## 3. Licensing
|
||||
@@ -29,19 +29,18 @@ All code released by the Elixir team is licensed under the
|
||||
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
|
||||
licenses are recognized as permissible in this project:
|
||||
|
||||
- The Unicode license, as documented at
|
||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
||||
|
||||
- The Elixir Trademark Policy, as documented at
|
||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
||||
- The Unicode license, as documented at
|
||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
||||
- The Elixir Trademark Policy, as documented at
|
||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
||||
|
||||
These licenses are considered acceptable for any files or code that form part of
|
||||
an Elixir repository. If a contribution requires a different license, it must
|
||||
either be rejected or prompt an update to this policy.
|
||||
|
||||
## 4. Contributing to the Elixir repository
|
||||
## 4. Contributing to Elixir Projects
|
||||
|
||||
Any code contributed to the Elixir repository must fall under one of the accepted
|
||||
Any code contributed to Elixir repositories must fall under one of the accepted
|
||||
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
|
||||
other license will be rejected unless this policy is formally revised to include
|
||||
that license. All files except those specifically exempted (e.g., certain test
|
||||
@@ -52,9 +51,13 @@ configuration and undergo review.
|
||||
|
||||
Contributions must not introduce executable binary files into the codebase.
|
||||
|
||||
Every Elixir project within the organization will have an automated GitHub
|
||||
Action to enforce these rules. This mechanism aids in detecting non-compliant
|
||||
licenses or files early in the review process.
|
||||
|
||||
## 5. Preservation of Copyright and License Information
|
||||
|
||||
Any third-party code incorporated into the Elixir repository must retain original
|
||||
Any third-party code incorporated into Elixir projects must retain original
|
||||
copyright and license headers. If no such headers exist in the source, they must
|
||||
be added. This practice ensures that original authors receive proper credit and
|
||||
that the licensing lineage is preserved.
|
||||
@@ -162,4 +165,4 @@ necessary, by the EEF CISO. Any significant changes will be communicated to
|
||||
contributors and made publicly available.
|
||||
|
||||
*Effective Date: 2025-02-20*
|
||||
*Last Reviewed: 2025-11-20*
|
||||
*Last Reviewed: 2025-02-20*
|
||||
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
|
||||
comment and we can always reopen the issue.
|
||||
|
||||
By keeping the overall issues tracker tidy and organized, the community
|
||||
can easily peek at what is coming in new releases and also get involved
|
||||
can easily peak at what is coming in new releases and also get involved
|
||||
by commenting on existing issues and submitting pull requests. Please
|
||||
remember to keep the tone positive and be kind! For more information,
|
||||
see the [Code of Conduct][1].
|
||||
@@ -122,10 +122,9 @@ 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.
|
||||
|
||||
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).
|
||||
code, and submit pull requests. We also include information on our review
|
||||
process, licensing requirements, and helpful tips to ensure a smooth
|
||||
contribution experience.
|
||||
|
||||
## Development links
|
||||
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
|
||||
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
||||
|
||||
## Creating a new vMAJOR.MINOR branch (usually before first rc)
|
||||
## Creating a new vMAJOR.MINOR branch (before first rc)
|
||||
|
||||
### In the new branch
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
### Back in main
|
||||
|
||||
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat
|
||||
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
|
||||
|
||||
2. Start new /CHANGELOG.md
|
||||
|
||||
|
||||
+3
-3
@@ -12,15 +12,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.19 | Bug fixes and security patches
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.4
|
||||
ELIXIR_VERSION=1.19.6
|
||||
|
||||
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.4
|
||||
set ELIXIR_VERSION=1.19.6
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+1
-2
@@ -293,7 +293,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
|
||||
@spec eval_string(String.t(), keyword, [compile_opt]) :: String.t()
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
@@ -364,7 +364,6 @@ defmodule EEx do
|
||||
### Helpers
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
|
||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
|
||||
+14
-27
@@ -96,7 +96,7 @@ defmodule EEx.Compiler do
|
||||
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
|
||||
"please remove \"#{marker}\""
|
||||
|
||||
IO.warn(message, file: state.file, line: line, column: column)
|
||||
:elixir_errors.erl_warn({line, column}, state.file, message)
|
||||
~c""
|
||||
else
|
||||
marker
|
||||
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
|
||||
file: file,
|
||||
source: source,
|
||||
line: line,
|
||||
quoted: %{},
|
||||
quoted: [],
|
||||
parser_options: [indentation: indentation] ++ parser_options,
|
||||
indentation: indentation
|
||||
}
|
||||
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
|
||||
state.parser_options
|
||||
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
buffer = handle_expr(buffer, mark, expr, meta, state)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -366,17 +366,17 @@ defmodule EEx.Compiler do
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[{contents, start_line, start_column} | scope],
|
||||
%{state | quoted: %{}, line: line}
|
||||
%{state | quoted: [], line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
buffer = handle_expr(buffer, mark, contents, meta, state)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
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!(:lists.flatten(wrapped), options)
|
||||
tuples = Code.string_to_quoted!(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(:lists.flatten(content), line, column, state)
|
||||
expr_meta = non_whitespace_meta(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 = 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))}
|
||||
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]}
|
||||
|
||||
{count, new_state}
|
||||
end
|
||||
@@ -479,7 +479,8 @@ defmodule EEx.Compiler do
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
Map.fetch!(quoted, key)
|
||||
{^key, value} = List.keyfind(quoted, key, 0)
|
||||
value
|
||||
end
|
||||
|
||||
defp insert_quoted({left, line, right}, quoted) do
|
||||
@@ -512,20 +513,6 @@ 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,18 +575,6 @@ 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
|
||||
|
||||
@@ -8,13 +8,8 @@
|
||||
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
|
||||
CoverageRecorder.maybe_record("eex")
|
||||
|
||||
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: []
|
||||
|
||||
ex_unit_opts =
|
||||
[
|
||||
trace: !!System.get_env("TRACE"),
|
||||
include: line_include,
|
||||
exclude: line_exclude
|
||||
] ++ maybe_seed_opt
|
||||
|
||||
ExUnit.start(ex_unit_opts)
|
||||
ExUnit.start(
|
||||
trace: !!System.get_env("TRACE"),
|
||||
include: line_include,
|
||||
exclude: line_exclude
|
||||
)
|
||||
|
||||
@@ -873,6 +873,11 @@ defmodule Access do
|
||||
...> end)
|
||||
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
||||
|
||||
An error is raised if the predicate is not a function or is of the incorrect arity:
|
||||
|
||||
iex> get_in([], [Access.filter(5)])
|
||||
** (FunctionClauseError) no function clause matching in Access.filter/1
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
||||
@@ -1091,11 +1096,6 @@ defmodule Access do
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
|
||||
when index > last do
|
||||
{:lists.reverse(gets), :lists.reverse(updates, rest)}
|
||||
end
|
||||
|
||||
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||
if index in range do
|
||||
case next.(head) do
|
||||
@@ -1154,6 +1154,11 @@ defmodule Access do
|
||||
...> end)
|
||||
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
||||
|
||||
An error is raised if the predicate is not a function or is of the incorrect arity:
|
||||
|
||||
iex> get_in([], [Access.find(5)])
|
||||
** (FunctionClauseError) no function clause matching in Access.find/1
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
|
||||
|
||||
@@ -507,7 +507,7 @@ defmodule Application do
|
||||
of all loaded applications. Returns `nil` if
|
||||
the module is not listed in any application spec.
|
||||
"""
|
||||
@spec get_application(module) :: app | nil
|
||||
@spec get_application(atom) :: atom | nil
|
||||
def get_application(module) when is_atom(module) do
|
||||
case :application.get_application(module) do
|
||||
{:ok, app} -> app
|
||||
@@ -680,13 +680,13 @@ defmodule Application do
|
||||
## Examples
|
||||
|
||||
`get_env/3` is commonly used to read the configuration of your OTP applications.
|
||||
Since Mix configurations are commonly used to configure applications (including
|
||||
your dependencies), we will use this as a point of illustration.
|
||||
Since Mix configurations are commonly used to configure applications, we will use
|
||||
this as a point of illustration.
|
||||
|
||||
Consider a new application `:my_app`. `:my_app` contains a database engine which
|
||||
supports a pool of databases. The database engine needs to know the configuration for
|
||||
each of those databases, and that configuration is supplied by key-value pairs in
|
||||
environment of `:my_app`. For example, your `config/runtime.exs` file might have:
|
||||
environment of `:my_app`.
|
||||
|
||||
config :my_app, Databases.RepoOne,
|
||||
# A database configuration
|
||||
@@ -696,7 +696,7 @@ defmodule Application do
|
||||
config :my_app, Databases.RepoTwo,
|
||||
# Another database configuration (for the same OTP app)
|
||||
ip: "localhost",
|
||||
port: 20_717
|
||||
port: 20717
|
||||
|
||||
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
|
||||
|
||||
@@ -714,11 +714,6 @@ defmodule Application do
|
||||
config = Application.get_env(:my_app, Databases.RepoOne)
|
||||
config[:ip]
|
||||
|
||||
The sample `config/runtime.exs` above could be used both for `:my_app` to
|
||||
configure itself but also to allow any application that depends on `:my_app`
|
||||
to configure how it works. However, one should keep in mind the caveats described
|
||||
in the `Application` module documentation: the application environment is global
|
||||
state which should be avoided if possible.
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) when is_atom(app) do
|
||||
@@ -819,7 +814,7 @@ defmodule Application do
|
||||
stick after the application is loaded and also on application reload.
|
||||
"""
|
||||
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
@@ -861,7 +856,7 @@ defmodule Application do
|
||||
It receives the same options as `put_env/4`. Returns `:ok`.
|
||||
"""
|
||||
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
@@ -908,13 +903,13 @@ defmodule Application do
|
||||
@doc """
|
||||
Ensures the given `app` or `apps` and their child applications are started.
|
||||
|
||||
The second argument is either the `t:restart_type/0` (for consistency with
|
||||
The second argument is either the `t:restart_type/1` (for consistency with
|
||||
`start/2`) or a keyword list.
|
||||
|
||||
## Options
|
||||
|
||||
* `:type` - if the application should be started `:temporary` (default),
|
||||
`:permanent`, or `:transient`. See `t:restart_type/0` for more information.
|
||||
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
|
||||
|
||||
* `:mode` - (since v1.15.0) if the applications should be started serially
|
||||
(`:serial`, default) or concurrently (`:concurrent`).
|
||||
@@ -926,11 +921,11 @@ defmodule Application do
|
||||
{:ok, [app]} | {:error, term}
|
||||
def ensure_all_started(app_or_apps, type_or_opts \\ [])
|
||||
|
||||
def ensure_all_started(app_or_apps, type) when is_atom(type) do
|
||||
ensure_all_started(app_or_apps, type: type)
|
||||
def ensure_all_started(app, type) when is_atom(type) do
|
||||
ensure_all_started(app, type: type)
|
||||
end
|
||||
|
||||
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do
|
||||
def ensure_all_started(app, opts) when is_atom(app) do
|
||||
ensure_all_started([app], opts)
|
||||
end
|
||||
|
||||
@@ -1061,8 +1056,7 @@ defmodule Application do
|
||||
Returns a list with information about the applications which are currently running.
|
||||
"""
|
||||
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
|
||||
def started_applications(timeout \\ 5000)
|
||||
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
|
||||
def started_applications(timeout \\ 5000) do
|
||||
:application.which_applications(timeout)
|
||||
end
|
||||
|
||||
|
||||
+54
-278
@@ -155,255 +155,6 @@ 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 singleton term. Trick lifted from
|
||||
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
|
||||
@swar_ge_slash 0x51515151515151
|
||||
|
||||
# base64's '+', '-' and '_' border no other range, so each is checked as the
|
||||
# degenerate range [c, c]. Mycroft zero-byte detection is one operation
|
||||
# cheaper per singleton but not exact per lane: subtracting 0x01..01 makes a
|
||||
# zero lane borrow from the lane above it, and a lane holding 1 turns that
|
||||
# borrow into 0xFF, so bytes equal to `c bxor 0x01` read as matches too.
|
||||
@swar_ge_plus 0x55555555555555
|
||||
@swar_gt_plus 0x54545454545454
|
||||
@swar_ge_dash 0x53535353535353
|
||||
@swar_gt_dash 0x52525252525252
|
||||
@swar_ge_underscore 0x21212121212121
|
||||
@swar_gt_underscore 0x20202020202020
|
||||
|
||||
# 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 singleton. For url,
|
||||
# '-' and '_' are two more singletons.
|
||||
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_ge_plus, w + @swar_gt_plus)
|
||||
),
|
||||
@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_ge_dash, w + @swar_gt_dash),
|
||||
bxor(w + @swar_ge_underscore, w + @swar_gt_underscore)
|
||||
)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 16 encoded string.
|
||||
|
||||
@@ -620,21 +371,25 @@ 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)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
|
||||
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(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
@@ -1006,19 +761,23 @@ 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)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
|
||||
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_name)(<<>>, _pad?), do: true
|
||||
|
||||
@@ -1104,6 +863,13 @@ 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)
|
||||
|
||||
@@ -1659,18 +1425,21 @@ 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)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
|
||||
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_name)(<<>>, _pad?), do: true
|
||||
|
||||
@@ -1750,6 +1519,13 @@ 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)
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Calendar do
|
||||
@strftime_max_width 1024
|
||||
|
||||
@moduledoc """
|
||||
This module defines the responsibilities for working with
|
||||
calendars, dates, times and datetimes in Elixir.
|
||||
@@ -531,7 +529,6 @@ defmodule Calendar do
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
(maximum #{@strftime_max_width})
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
@@ -670,13 +667,9 @@ defmodule Calendar do
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
width = (width || 0) * 10 + (digit - ?0)
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
if width > @strftime_max_width do
|
||||
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
|
||||
end
|
||||
|
||||
parse_modifiers(rest, width, pad, parser_data)
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
|
||||
@@ -53,7 +53,7 @@ defmodule Date do
|
||||
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
|
||||
14716
|
||||
|
||||
iex> Date.add(~D[1970-01-01], 14_716)
|
||||
iex> Date.add(~D[1970-01-01], 14716)
|
||||
~D[2010-04-17]
|
||||
|
||||
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
|
||||
|
||||
@@ -59,20 +59,11 @@ defmodule Date.Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
member? =
|
||||
quote generated: true do
|
||||
member?(
|
||||
%{
|
||||
__struct__: Date.Range,
|
||||
first_in_iso_days: var!(first_days),
|
||||
last_in_iso_days: var!(last_days)
|
||||
} =
|
||||
var!(date_range),
|
||||
var!(date)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(member?) do
|
||||
def member?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range,
|
||||
date
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
member?(Map.put(date_range, :step, step), date)
|
||||
end
|
||||
@@ -227,11 +218,11 @@ defmodule Date.Range do
|
||||
defimpl Inspect do
|
||||
import Kernel, except: [inspect: 2]
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do
|
||||
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
|
||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
|
||||
end
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do
|
||||
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
|
||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
|
||||
end
|
||||
|
||||
|
||||
@@ -1622,9 +1622,8 @@ defmodule DateTime do
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function relies on a contiguous representation of time,
|
||||
ignoring timezone changes. For example, if you add one day when there
|
||||
|
||||
@@ -232,7 +232,7 @@ defmodule Duration do
|
||||
@doc """
|
||||
Adds units of given durations `d1` and `d2`.
|
||||
|
||||
Respects the highest microsecond precision of the two.
|
||||
Respects the the highest microsecond precision of the two.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -262,7 +262,7 @@ defmodule Duration do
|
||||
@doc """
|
||||
Subtracts units of given durations `d1` and `d2`.
|
||||
|
||||
Respects the highest microsecond precision of the two.
|
||||
Respects the the highest microsecond precision of the two.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
+153
-83
@@ -182,7 +182,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
@type day_of_year :: 1..366
|
||||
@type quarter_of_year :: 1..4
|
||||
@type year_of_era :: {1..10_000, era}
|
||||
@type year_of_era :: {1..10000, era}
|
||||
|
||||
@seconds_per_minute 60
|
||||
@seconds_per_hour 60 * 60
|
||||
@@ -196,28 +196,14 @@ defmodule Calendar.ISO do
|
||||
@ext_date_sep ?-
|
||||
@ext_time_sep ?:
|
||||
|
||||
@days_per_nonleap_year 365
|
||||
@days_per_leap_year 366
|
||||
|
||||
# The ISO epoch starts, in this implementation,
|
||||
# with ~D[0000-01-01]. Era "1" starts
|
||||
# on ~D[0001-01-01] which is 366 days later.
|
||||
@iso_epoch 366
|
||||
|
||||
# Constants for date calculations using 400-year era cycles.
|
||||
# The algorithm uses a March-based year where March 1 is day 0.
|
||||
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
|
||||
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
|
||||
@days_per_year 365
|
||||
@years_per_era 400
|
||||
@days_per_era @years_per_era * @days_per_year + 97
|
||||
@days_per_4_years 4 * @days_per_year
|
||||
@days_per_100_years 100 * @days_per_year + 24
|
||||
@march_1_offset 31 + 29
|
||||
@unix_epoch_days 719_528
|
||||
|
||||
# Month calculation constants: in a March-based year, each 5-month
|
||||
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
|
||||
@days_per_5_months 153
|
||||
@months_per_cycle 5
|
||||
|
||||
[match_basic_date, match_ext_date, guard_date, read_date] =
|
||||
quote do
|
||||
[
|
||||
@@ -666,12 +652,12 @@ defmodule Calendar.ISO do
|
||||
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
|
||||
|
||||
{{year, month, day}, {hour, minute, second, _}} =
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
|
||||
{0, day_fraction} ->
|
||||
{{year, month, day}, time_from_day_fraction(day_fraction)}
|
||||
|
||||
{extra_days, day_fraction} ->
|
||||
base_days = valid_date_to_iso_days(year, month, day)
|
||||
base_days = date_to_iso_days(year, month, day)
|
||||
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
|
||||
end
|
||||
|
||||
@@ -798,13 +784,13 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86_400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
|
||||
{0, 1, 1, 0, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86_400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
|
||||
{2000, 1, 1, 0, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43_200, 86_400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
|
||||
{2000, 1, 1, 12, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86_400_000_000}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
|
||||
{-1, 1, 1, 0, 0, 0, {0, 6}}
|
||||
|
||||
"""
|
||||
@@ -892,52 +878,28 @@ defmodule Calendar.ISO do
|
||||
|
||||
# Converts year, month, day to count of days since 0000-01-01.
|
||||
@doc false
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
def date_to_iso_days(0, 1, 1) do
|
||||
0
|
||||
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
|
||||
def date_to_iso_days(1970, 1, 1) do
|
||||
719_528
|
||||
end
|
||||
|
||||
defp valid_date_to_iso_days(year, month, day) do
|
||||
y = if month <= 2, do: year - 1, else: year
|
||||
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
|
||||
year_of_era = y - era * @years_per_era
|
||||
month_prime = if month > 2, do: month - 3, else: month + 9
|
||||
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
|
||||
day_of_era =
|
||||
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
|
||||
|
||||
era * @days_per_era + day_of_era + @march_1_offset
|
||||
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
|
||||
1
|
||||
end
|
||||
|
||||
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
|
||||
@doc false
|
||||
def date_from_iso_days(days) do
|
||||
z = days - @march_1_offset
|
||||
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
|
||||
day_of_era = z - era * @days_per_era
|
||||
|
||||
year_of_era =
|
||||
div(
|
||||
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
|
||||
div(day_of_era, @days_per_era - 1),
|
||||
@days_per_year
|
||||
)
|
||||
|
||||
day_of_year =
|
||||
day_of_era -
|
||||
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
|
||||
|
||||
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
|
||||
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
|
||||
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
|
||||
year = year_of_era + era * @years_per_era
|
||||
year = if month <= 2, do: year + 1, else: year
|
||||
|
||||
{year, month, day}
|
||||
{year, day_of_year} = days_to_year(days)
|
||||
extra_day = if leap_year?(year), do: 1, else: 0
|
||||
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
|
||||
{year, month, day_in_month + 1}
|
||||
end
|
||||
|
||||
defp div_rem(int1, int2) do
|
||||
@@ -951,9 +913,6 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
|
||||
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
|
||||
|
||||
@doc """
|
||||
Returns how many days there are in the given year-month.
|
||||
|
||||
@@ -1174,7 +1133,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec year_of_era(year) :: {1..10_000, era}
|
||||
@spec year_of_era(year) :: {1..10000, era}
|
||||
def year_of_era(year) when is_year_CE(year), do: {year, 1}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
|
||||
|
||||
@@ -1200,7 +1159,7 @@ defmodule Calendar.ISO do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@impl true
|
||||
@spec year_of_era(year, month, day) :: {1..10_000, era}
|
||||
@spec year_of_era(year, month, day) :: {1..10000, era}
|
||||
def year_of_era(year, _month, _day), do: year_of_era(year)
|
||||
|
||||
@doc """
|
||||
@@ -1745,11 +1704,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86_400_000_000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86400000000}})
|
||||
{0, {0, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {43_200_000_000, 86_400_000_000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {43200000000, 86400000000}})
|
||||
{730485, {0, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {46_800_000_000, 86_400_000_000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {46800000000, 86400000000}})
|
||||
{730485, {0, 86400000000}}
|
||||
|
||||
"""
|
||||
@@ -1765,11 +1724,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86_400_000_000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86400000000}})
|
||||
{0, {86399999999, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {43_200_000_000, 86_400_000_000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {43200000000, 86400000000}})
|
||||
{730485, {86399999999, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {46_800_000_000, 86_400_000_000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {46800000000, 86400000000}})
|
||||
{730485, {86399999999, 86400000000}}
|
||||
|
||||
"""
|
||||
@@ -1878,7 +1837,7 @@ defmodule Calendar.ISO do
|
||||
@doc false
|
||||
def shift_days({year, month, day}, days) do
|
||||
{year, month, day} =
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
date_to_iso_days(year, month, day)
|
||||
|> Kernel.+(days)
|
||||
|> date_from_iso_days()
|
||||
|
||||
@@ -1889,7 +1848,7 @@ defmodule Calendar.ISO do
|
||||
months_in_year = 12
|
||||
total_months = year * months_in_year + month + months - 1
|
||||
|
||||
new_year = floor_div_positive_divisor(total_months, months_in_year)
|
||||
new_year = Integer.floor_div(total_months, months_in_year)
|
||||
|
||||
new_month =
|
||||
case rem(total_months, months_in_year) + 1 do
|
||||
@@ -1929,7 +1888,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
|
||||
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(86_400, :second, unit)
|
||||
ppd = System.convert_time_unit(86400, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, value, ppd)
|
||||
end
|
||||
|
||||
@@ -1978,7 +1937,7 @@ defmodule Calendar.ISO do
|
||||
}) do
|
||||
[
|
||||
month: year * 12 + month,
|
||||
second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second,
|
||||
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
|
||||
microsecond: microsecond
|
||||
]
|
||||
end
|
||||
@@ -2012,7 +1971,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
if total in @unix_range_microseconds do
|
||||
microseconds = Integer.mod(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
|
||||
precision = precision_for_unit(unit)
|
||||
{date, time} = iso_seconds_to_datetime(seconds)
|
||||
{:ok, date, time, {microseconds, precision}}
|
||||
@@ -2134,12 +2093,9 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp leap_day_offset(_year, month) when month < 3, do: 0
|
||||
|
||||
defp leap_day_offset(year, _month) do
|
||||
if leap_year?(year), do: 1, else: 0
|
||||
end
|
||||
|
||||
# Note that this function does not add the extra leap day for a leap year.
|
||||
# If you want to add that leap day when appropriate,
|
||||
# add the result of leap_day_offset/2 to the result of days_before_month/1.
|
||||
defp days_before_month(1), do: 0
|
||||
defp days_before_month(2), do: 31
|
||||
defp days_before_month(3), do: 59
|
||||
@@ -2153,6 +2109,120 @@ defmodule Calendar.ISO do
|
||||
defp days_before_month(11), do: 304
|
||||
defp days_before_month(12), do: 334
|
||||
|
||||
defp leap_day_offset(_year, month) when month < 3, do: 0
|
||||
|
||||
defp leap_day_offset(year, _month) do
|
||||
if leap_year?(year), do: 1, else: 0
|
||||
end
|
||||
|
||||
defp days_to_year(days) when days < 0 do
|
||||
year_estimate = -div(-days, @days_per_nonleap_year) - 1
|
||||
|
||||
{year, days_before_year} =
|
||||
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
|
||||
|
||||
leap_year_pad = if leap_year?(year), do: 1, else: 0
|
||||
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
|
||||
end
|
||||
|
||||
defp days_to_year(days) do
|
||||
year_estimate = div(days, @days_per_nonleap_year)
|
||||
|
||||
{year, days_before_year} =
|
||||
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
|
||||
|
||||
{year, days - days_before_year}
|
||||
end
|
||||
|
||||
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
|
||||
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
|
||||
end
|
||||
|
||||
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
|
||||
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
|
||||
end
|
||||
|
||||
defp days_to_year(year, _days1, days2) do
|
||||
{year, days2}
|
||||
end
|
||||
|
||||
defp days_to_end_of_epoch(year) when year < 0 do
|
||||
previous_year = year + 1
|
||||
|
||||
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
|
||||
previous_year * @days_per_nonleap_year
|
||||
end
|
||||
|
||||
defp days_in_previous_years(0), do: 0
|
||||
|
||||
# A concise version of the algorithm would use floor_div instead of div.
|
||||
# However, floor_div would check the operands on every operation.
|
||||
# We optimize this by providing a positive and negative version of each algorithm.
|
||||
defp days_in_previous_years(year) when year > 0 do
|
||||
previous_year = year - 1
|
||||
|
||||
div(previous_year, 4) - div(previous_year, 100) +
|
||||
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
|
||||
@days_per_leap_year
|
||||
end
|
||||
|
||||
defp days_in_previous_years(year) when year < 0 do
|
||||
previous_year = year - 1
|
||||
|
||||
div(year, 4) - div(year, 100) +
|
||||
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
|
||||
@days_per_leap_year
|
||||
end
|
||||
|
||||
# Note that 0 is the first day of the month.
|
||||
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
|
||||
{1, day_of_year}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
|
||||
{2, day_of_year - 31}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
|
||||
{3, day_of_year - (59 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
|
||||
{4, day_of_year - (90 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
|
||||
{5, day_of_year - (120 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
|
||||
{6, day_of_year - (151 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
|
||||
{7, day_of_year - (181 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
|
||||
{8, day_of_year - (212 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
|
||||
{9, day_of_year - (243 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
|
||||
{10, day_of_year - (273 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
|
||||
{11, day_of_year - (304 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) do
|
||||
{12, day_of_year - (334 + extra_day)}
|
||||
end
|
||||
|
||||
defp iso_seconds_to_datetime(seconds) do
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
|
||||
|
||||
@@ -402,9 +402,8 @@ defmodule NaiveDateTime do
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -715,18 +714,16 @@ 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: _
|
||||
} = _naive_datetime
|
||||
) do
|
||||
def to_date(%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
}) do
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
@@ -743,18 +740,16 @@ 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
|
||||
} = _naive_datetime
|
||||
) do
|
||||
def to_time(%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}) do
|
||||
%Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
@@ -1150,18 +1145,16 @@ 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}
|
||||
} = _naive_datetime
|
||||
) do
|
||||
def to_gregorian_seconds(%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}) do
|
||||
{days, day_fraction} =
|
||||
calendar.naive_datetime_to_iso_days(
|
||||
year,
|
||||
|
||||
@@ -514,9 +514,8 @@ defmodule Time do
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
Note the result value represents the time of day, meaning that it is cyclic,
|
||||
for instance, it will never go over 24 hours for the ISO calendar.
|
||||
|
||||
+72
-123
@@ -286,25 +286,19 @@ defmodule Code do
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
static_atoms_encoder: (atom() -> term()),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for evaluation environment, accepted by `env_for_eval/1`.
|
||||
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
|
||||
"""
|
||||
@type env_eval_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, pos_integer()}
|
||||
| {:module, module()}
|
||||
|
||||
@typedoc """
|
||||
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
|
||||
and `eval_quoted_with_env/4`.
|
||||
"""
|
||||
@type eval_opt ::
|
||||
{:prune_binding, boolean()}
|
||||
| {:dbg_callback, {module(), atom(), list()}}
|
||||
@type env_eval_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
module: module(),
|
||||
prune_binding: boolean()
|
||||
]
|
||||
|
||||
@boolean_compiler_options [
|
||||
:docs,
|
||||
@@ -318,12 +312,7 @@ defmodule Code do
|
||||
|
||||
@available_compiler_options @boolean_compiler_options ++
|
||||
@list_compiler_options ++
|
||||
[
|
||||
:on_undefined_variable,
|
||||
:infer_signatures,
|
||||
:no_warn_undefined,
|
||||
:module_definition
|
||||
]
|
||||
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
|
||||
|
||||
@doc """
|
||||
Lists all required files.
|
||||
@@ -579,11 +568,9 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
It accepts the same options as both `env_for_eval/1` and
|
||||
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
|
||||
as third argument, so the evaluation happens within that environment.
|
||||
|
||||
## Return
|
||||
It accepts the same options as `env_for_eval/1`. Additionally, you may
|
||||
also pass an environment as second argument, so the evaluation happens
|
||||
within that environment.
|
||||
|
||||
Returns a tuple of the form `{value, binding}`, where `value` is the value
|
||||
returned from evaluating `string`. If an error occurs while evaluating
|
||||
@@ -613,11 +600,11 @@ defmodule Code do
|
||||
iex> Enum.sort(binding)
|
||||
[a: 3, b: 2]
|
||||
|
||||
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
|
||||
For convenience, you can pass `__ENV__/0` as the `opts` argument and
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
iex> require Integer, warn: false
|
||||
iex> require Integer
|
||||
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
|
||||
iex> result
|
||||
3
|
||||
@@ -625,28 +612,21 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
|
||||
{term, binding}
|
||||
def eval_string(string, binding \\ [], opts_or_env \\ [])
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
|
||||
validated_eval_string(string, binding, env)
|
||||
end
|
||||
|
||||
def eval_string(string, binding, opts) when is_list(opts) do
|
||||
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
|
||||
validated_eval_string(string, binding, opts)
|
||||
end
|
||||
|
||||
defp validate_binding(binding) when is_list(binding), do: binding
|
||||
|
||||
defp validate_binding(binding) do
|
||||
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
|
||||
end
|
||||
|
||||
defp validated_eval_string(string, binding, env, opts) do
|
||||
%{line: line, file: file} = env
|
||||
defp validated_eval_string(string, binding, opts_or_env) do
|
||||
%{line: line, file: file} = env = env_for_eval(opts_or_env)
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -1149,8 +1129,7 @@ defmodule Code do
|
||||
returned quoted expressions (instead of evaluated).
|
||||
|
||||
See `eval_string/3` for a description of arguments and return types.
|
||||
It accepts the same options as both `env_for_eval/1` and
|
||||
`eval_quoted_with_env/4`.
|
||||
The options are described under `env_for_eval/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1172,20 +1151,11 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
|
||||
{term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
|
||||
{value, binding, _env} =
|
||||
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
|
||||
|
||||
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
|
||||
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
|
||||
end
|
||||
|
||||
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
||||
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
|
||||
end
|
||||
|
||||
defp eval_quoted(quoted, binding, env, opts) do
|
||||
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -1213,9 +1183,14 @@ defmodule Code do
|
||||
|
||||
* `:module` - the module to run the environment on
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
|
||||
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
|
||||
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
|
||||
|
||||
@doc """
|
||||
@@ -1229,19 +1204,11 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
|
||||
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
|
||||
used in the evaluated code. It must be a `{module, function, args}`
|
||||
tuple, see `dbg/2` for more details.
|
||||
It accepts the same options as `env_for_eval/1`.
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
|
||||
{term, binding, Macro.Env.t()}
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
|
||||
when is_list(binding) do
|
||||
@@ -1319,7 +1286,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 line and column. It must return
|
||||
keyword list with the current file, line and column. It must return
|
||||
`{:ok, token :: term} | {:error, reason :: binary}`.
|
||||
|
||||
The encoder function is supposed to create an atom from the given
|
||||
@@ -1358,7 +1325,14 @@ defmodule Code do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
|
||||
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
|
||||
{:ok, tokens} ->
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:error, _error_msg} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1434,7 +1408,8 @@ defmodule Code do
|
||||
Process.put(:code_formatter_comments, [])
|
||||
opts = [preserve_comments: &preserve_comments/5] ++ opts
|
||||
|
||||
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
|
||||
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
comments = Enum.reverse(Process.get(:code_formatter_comments))
|
||||
{:ok, forms, comments}
|
||||
end
|
||||
@@ -1634,19 +1609,13 @@ defmodule Code do
|
||||
nil
|
||||
|
||||
:proceed ->
|
||||
try do
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
catch
|
||||
kind, reason ->
|
||||
:elixir_code_server.call({:release, file})
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
end
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1741,6 +1710,9 @@ defmodule Code do
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:debug_info` - when `true`, retains debug information in the compiled
|
||||
module. This option can also be overridden per module using the `@compile`
|
||||
directive. Defaults to `true`.
|
||||
@@ -1756,9 +1728,6 @@ defmodule Code do
|
||||
via the `:test_elixirc_options` project configuration, as there is
|
||||
typically no need to store debug chunks for test files.
|
||||
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
|
||||
when a protocol has already been consolidated and a new implementation is added.
|
||||
Defaults to `false`.
|
||||
@@ -1770,37 +1739,24 @@ defmodule Code do
|
||||
should be using during type inference. When `false`, it disables module-local
|
||||
signature inference used when type checking remote calls to the compiled
|
||||
module. Type checking will be executed regardless of the value of this option.
|
||||
Mix projects will set this option to your dependencies list in dev/prod, and
|
||||
it will disable this option during test (as there is typically no need to infer
|
||||
signature for test files). Outside of Mix projects, it defaults to `[:elixir]`.
|
||||
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
|
||||
|
||||
* `:module_definition` (since v1.20.0) - stores if the module definition should
|
||||
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
|
||||
`.beam` file written to disk, only how the contents inside `defmodule` are
|
||||
executed. Using the `:interpreted` mode may offer better compilation times for
|
||||
large projects, especially on machines with high core count, however, it comes
|
||||
with some downsides:
|
||||
When setting this option, we recommend running `mix clean` so the current module
|
||||
may be compiled from scratch. `mix test` automatically disables this option via
|
||||
the `:test_elixirc_options` project configuration, as there is typically no need
|
||||
to infer signatures for test files.
|
||||
|
||||
* Errors during compilation may have less precise stacktraces
|
||||
|
||||
* Anonymous functions within `defmodule` can have only up to 20 arguments.
|
||||
If this is an issue, you can use maps or tuples to group the data.
|
||||
Note the functions themselves inside `defmodule`, such as the ones defined
|
||||
inside `def` and friends, can still have up to 255 arguments
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
|
||||
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
|
||||
tuples that will not emit warnings that the module or function does not exist
|
||||
at compilation time. Pass atom `:all` to skip warning for all undefined
|
||||
functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
|
||||
|
||||
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
|
||||
When `:raise` (the default), undefined variables will trigger a compilation
|
||||
error. You may be set it to `:warn` if you want undefined variables to
|
||||
emit a warning and expand as to a local call to the zero-arity function
|
||||
of the same name (for example, `node` would be expanded as `node()`).
|
||||
This `:warn` behavior only exists for compatibility reasons when working
|
||||
with old dependencies, its usage is discouraged and it will be removed
|
||||
in future releases.
|
||||
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
|
||||
compilation. See the module docs for more information. Defaults to `[]`.
|
||||
|
||||
* `:parser_options` (since v1.10.0) - a keyword list of options to be given
|
||||
to the parser when compiling files. It accepts the same options as
|
||||
@@ -1811,12 +1767,14 @@ defmodule Code do
|
||||
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
|
||||
latter is used for other purposes beyond compilation.
|
||||
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
|
||||
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
|
||||
compilation. See the module docs for more information. Defaults to `[]`.
|
||||
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
|
||||
When `:raise` (the default), undefined variables will trigger a compilation
|
||||
error. You may be set it to `:warn` if you want undefined variables to
|
||||
emit a warning and expand as to a local call to the zero-arity function
|
||||
of the same name (for example, `node` would be expanded as `node()`).
|
||||
This `:warn` behavior only exists for compatibility reasons when working
|
||||
with old dependencies, its usage is discouraged and it will be removed
|
||||
in future releases.
|
||||
|
||||
It always returns `:ok`. Raises an error for invalid options.
|
||||
|
||||
@@ -1856,15 +1814,6 @@ defmodule Code do
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(:module_definition, value) do
|
||||
if value not in [:interpreted, :compiled] do
|
||||
raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
:elixir_config.put(:module_definition, value)
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(:infer_signatures, value) do
|
||||
value =
|
||||
cond do
|
||||
|
||||
@@ -204,7 +204,6 @@ defmodule Code.Fragment do
|
||||
| {:local_arity, charlist}
|
||||
| {:local_call, charlist}
|
||||
| {:anonymous_call, inside_caller}
|
||||
| {:capture_arg, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:operator, charlist}
|
||||
| {:operator_arity, charlist}
|
||||
@@ -1235,43 +1234,14 @@ defmodule Code.Fragment do
|
||||
the cursor. This is necessary to correctly complete anonymous functions
|
||||
and the left-hand side of `->`
|
||||
|
||||
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
|
||||
(see "Tracking sigils" section below)
|
||||
|
||||
## Tracking sigils
|
||||
|
||||
The `:preserve_sigils` option can be used to track cursor positions inside
|
||||
a sigil.
|
||||
|
||||
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
|
||||
as the second argument:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
|
||||
{:ok,
|
||||
{:sigil_r, [delimiter: "/", line: 1],
|
||||
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
|
||||
|
||||
In case the sigil is completed and has zero or more modifiers, the cursor will
|
||||
be nested in the list, with all previous delimiters specified:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
|
||||
{:ok,
|
||||
{:sigil_r, [delimiter: "/", line: 1],
|
||||
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
|
||||
|
||||
If the cursor is after the sigil, then it is discarded as everything else:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
|
||||
{:ok, {:__cursor__, [line: 1], []}}
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||
def container_cursor_to_quoted(fragment, opts \\ []) do
|
||||
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
|
||||
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
|
||||
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
|
||||
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
|
||||
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
|
||||
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
@@ -1291,10 +1261,7 @@ defmodule Code.Fragment do
|
||||
end
|
||||
|
||||
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
|
||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, forms}
|
||||
end
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
|
||||
tokens =
|
||||
@@ -1302,7 +1269,7 @@ defmodule Code.Fragment do
|
||||
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
|
||||
true <- maybe_missing_stab?(rev_tokens, true),
|
||||
opts =
|
||||
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
|
||||
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
|
||||
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
|
||||
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
|
||||
trailing_tokens =
|
||||
@@ -1321,12 +1288,10 @@ defmodule Code.Fragment do
|
||||
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
end
|
||||
|
||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, forms}
|
||||
end
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:error, info, _rest, _warnings, _so_far} ->
|
||||
{:error, :elixir_tokenizer.format_error(info)}
|
||||
{:error, :elixir.format_token_error(info)}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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, false)
|
||||
normalize_bitstring(quoted, state)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
@@ -89,7 +89,13 @@ defmodule Code.Normalizer do
|
||||
normalize_literal(:utf8, [], state)
|
||||
end
|
||||
|
||||
string = normalize_bitstring(string, state, state.escape)
|
||||
string =
|
||||
if state.escape do
|
||||
normalize_bitstring(string, state, true)
|
||||
else
|
||||
normalize_bitstring(string, state)
|
||||
end
|
||||
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||
end
|
||||
|
||||
@@ -112,7 +118,6 @@ 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)
|
||||
@@ -400,8 +405,7 @@ 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) do
|
||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
parts =
|
||||
@@ -423,17 +427,6 @@ 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,22 +69,6 @@ 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,27 +377,21 @@ defmodule Config do
|
||||
end
|
||||
end
|
||||
|
||||
defp validate!(config, file) when is_list(config) do
|
||||
Enum.each(config, fn
|
||||
defp validate!(config, file) do
|
||||
Enum.all?(config, fn
|
||||
{app, value} when is_atom(app) ->
|
||||
if not Keyword.keyword?(value) do
|
||||
if Keyword.keyword?(value) do
|
||||
true
|
||||
else
|
||||
raise ArgumentError,
|
||||
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
|
||||
"to return keyword list, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
||||
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
|
||||
_ ->
|
||||
false
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
|
||||
|
||||
## Examples
|
||||
|
||||
A dynamic supervisor is started with no children and often with a name:
|
||||
A dynamic supervisor is started with no children and often a name:
|
||||
|
||||
children = [
|
||||
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
|
||||
@@ -206,7 +206,6 @@ defmodule DynamicSupervisor do
|
||||
See `Supervisor` for more information about child specifications.
|
||||
"""
|
||||
@doc since: "1.6.1"
|
||||
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
|
||||
def child_spec(options) when is_list(options) do
|
||||
id =
|
||||
case Keyword.get(options, :name, DynamicSupervisor) do
|
||||
@@ -481,11 +480,11 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
Returns a list with information about all children.
|
||||
|
||||
Note that calling this function when supervising a large number
|
||||
of children under low memory conditions can bring the system down due to an
|
||||
out of memory error.
|
||||
of children under low memory conditions can cause an out of memory
|
||||
exception.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
|
||||
+54
-165
@@ -39,20 +39,6 @@ defprotocol Enumerable do
|
||||
`reduce/3` function. All other functions exist as optimizations paths
|
||||
for data structures that can implement certain properties in better
|
||||
than linear time.
|
||||
|
||||
## Default implementation for lists
|
||||
|
||||
Sometimes you may want to implement this protocol for a list contained
|
||||
in struct. This can be done by delegating to the `Enumerable.List` module
|
||||
in the `reduce/3` implementation and providing a straight-forward
|
||||
implementation for the remaining ones:
|
||||
|
||||
defimpl Enumerable, for: CustomStruct do
|
||||
def count(struct), do: {:ok, length(struct.items)}
|
||||
def member?(struct, value), do: {:ok, value in struct.items}
|
||||
def slice(struct), do: {:error, __MODULE__}
|
||||
def reduce(struct, acc, fun), do: Enumerable.List.reduce(struct.items, acc, fun)
|
||||
end
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
@@ -780,10 +766,6 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def count_until(_enumerable, limit) when is_integer(limit) do
|
||||
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`.
|
||||
|
||||
@@ -805,10 +787,6 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
|
||||
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enumerates the `enumerable`, returning a list where all consecutive
|
||||
duplicate elements are collapsed to a single element.
|
||||
@@ -829,7 +807,7 @@ defmodule Enum do
|
||||
"""
|
||||
@spec dedup(t) :: list
|
||||
def dedup(enumerable) when is_list(enumerable) do
|
||||
dedup_list(enumerable)
|
||||
dedup_list(enumerable, []) |> :lists.reverse()
|
||||
end
|
||||
|
||||
def dedup(enumerable) do
|
||||
@@ -973,8 +951,8 @@ defmodule Enum do
|
||||
## Examples
|
||||
|
||||
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
|
||||
some
|
||||
example
|
||||
"some"
|
||||
"example"
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -1236,7 +1214,7 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps the given `fun` over `enumerable` and flattens the result only one level deep.
|
||||
Maps the given `fun` over `enumerable` and flattens the result.
|
||||
|
||||
This function returns a new enumerable built by appending the result of invoking `fun`
|
||||
on each element of `enumerable` together; conceptually, this is similar to a
|
||||
@@ -1253,7 +1231,7 @@ defmodule Enum do
|
||||
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
|
||||
[[:a], [:b], [:c]]
|
||||
|
||||
This is frequently used to transform and filter in one pass, returning empty
|
||||
This is frequently used to to transform and filter in one pass, returning empty
|
||||
lists to exclude results:
|
||||
|
||||
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
|
||||
@@ -1284,16 +1262,13 @@ defmodule Enum do
|
||||
defp flat_reverse([], acc), do: acc
|
||||
|
||||
@doc """
|
||||
Maps and reduces an `enumerable`, flattening the results only one level deep.
|
||||
Maps and reduces an `enumerable`, flattening the given results (only one level deep).
|
||||
|
||||
It expects an accumulator and a function that receives each enumerable
|
||||
element, and must return a tuple containing a new enumerable (often a list)
|
||||
with the new accumulator or a tuple with `:halt` as first element and
|
||||
the accumulator as second.
|
||||
|
||||
Returns a 2-element tuple where the first element is the results flattened one level deep and
|
||||
the second element is the last accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> enumerable = 1..100
|
||||
@@ -1518,14 +1493,6 @@ defmodule Enum do
|
||||
to_list(enumerable)
|
||||
end
|
||||
|
||||
def into(enumerable, collectable) when is_struct(collectable, MapSet) do
|
||||
if MapSet.size(collectable) == 0 do
|
||||
MapSet.new(enumerable)
|
||||
else
|
||||
MapSet.new(enumerable) |> MapSet.union(collectable)
|
||||
end
|
||||
end
|
||||
|
||||
def into(%_{} = enumerable, collectable) do
|
||||
into_protocol(enumerable, collectable)
|
||||
end
|
||||
@@ -1602,12 +1569,8 @@ defmodule Enum do
|
||||
map(enumerable, transform)
|
||||
end
|
||||
|
||||
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do
|
||||
if MapSet.size(collectable) == 0 do
|
||||
MapSet.new(enumerable, transform)
|
||||
else
|
||||
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
|
||||
end
|
||||
def into(%_{} = enumerable, collectable, transform) do
|
||||
into_protocol(enumerable, collectable, transform)
|
||||
end
|
||||
|
||||
def into(enumerable, %_{} = collectable, transform) do
|
||||
@@ -1879,7 +1842,7 @@ defmodule Enum do
|
||||
Returns the maximal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
|
||||
By default, the comparison is done with the `>=` sorter function.
|
||||
If multiple elements are considered maximal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
maximal to be returned, the sorter function should not return true
|
||||
@@ -1946,7 +1909,7 @@ defmodule Enum do
|
||||
Returns the maximal element in the `enumerable` as calculated
|
||||
by the given `fun`.
|
||||
|
||||
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
|
||||
By default, the comparison is done with the `>=` sorter function.
|
||||
If multiple elements are considered maximal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
maximal to be returned, the sorter function should not return true
|
||||
@@ -2023,16 +1986,11 @@ defmodule Enum do
|
||||
operators work by using this function.
|
||||
"""
|
||||
@spec member?(t, element) :: boolean
|
||||
def member?(enumerable, element) do
|
||||
__in__(element, enumerable)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __in__(element, enumerable) when is_list(enumerable) do
|
||||
def member?(enumerable, element) when is_list(enumerable) do
|
||||
:lists.member(element, enumerable)
|
||||
end
|
||||
|
||||
def __in__(element, enumerable) do
|
||||
def member?(enumerable, element) do
|
||||
case Enumerable.member?(enumerable, element) do
|
||||
{:ok, element} when is_boolean(element) ->
|
||||
element
|
||||
@@ -2064,7 +2022,7 @@ defmodule Enum do
|
||||
Returns the minimal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
|
||||
By default, the comparison is done with the `<=` sorter function.
|
||||
If multiple elements are considered minimal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
minimal to be returned, the sorter function should not return true
|
||||
@@ -2131,7 +2089,7 @@ defmodule Enum do
|
||||
Returns the minimal element in the `enumerable` as calculated
|
||||
by the given `fun`.
|
||||
|
||||
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
|
||||
By default, the comparison is done with the `<=` sorter function.
|
||||
If multiple elements are considered minimal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
minimal to be returned, the sorter function should not return true
|
||||
@@ -2185,60 +2143,28 @@ defmodule Enum do
|
||||
|
||||
@doc """
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable.
|
||||
enumerable according to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the [`<`](`</2`) sorter function,
|
||||
as the function must not return true for equal elements.
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
|
||||
Calls the provided `empty_fallback` function and returns its value if
|
||||
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.min_max([2, 3, 1])
|
||||
{1, 3}
|
||||
|
||||
iex> Enum.min_max(["foo", "bar", "baz"])
|
||||
{"bar", "foo"}
|
||||
|
||||
iex> Enum.min_max([], fn -> {nil, nil} end)
|
||||
{nil, nil}
|
||||
|
||||
The fact this function uses Erlang's term ordering means that the
|
||||
comparison is structural and not semantic. Therefore, if you want
|
||||
to compare structs, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
iex> dates = [
|
||||
...> ~D[2019-01-01],
|
||||
...> ~D[2020-01-01],
|
||||
...> ~D[2018-01-01]
|
||||
...> ]
|
||||
iex> Enum.min_max(dates, Date)
|
||||
{~D[2018-01-01], ~D[2020-01-01]}
|
||||
|
||||
You can also pass a custom sorting function:
|
||||
|
||||
iex> Enum.min_max([2, 3, 1], &>/2)
|
||||
{3, 1}
|
||||
|
||||
Finally, if you don't want to raise on empty enumerables, you can pass
|
||||
the empty fallback:
|
||||
|
||||
iex> Enum.min_max([], fn -> nil end)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
|
||||
{element, element} | empty_result
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
|
||||
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
case Range.size(range) do
|
||||
0 ->
|
||||
empty_fallback.()
|
||||
@@ -2249,39 +2175,11 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def min_max(enumerable, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
min_max(enumerable, &</2, empty_fallback)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter) when is_atom(sorter) do
|
||||
min_max(enumerable, min_max_sort_fun(sorter))
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter) when is_function(sorter, 2) do
|
||||
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter, empty_fallback)
|
||||
when is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
|
||||
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
first_fun = &[&1 | &1]
|
||||
|
||||
reduce_fun = fn entry, [min | max] = acc ->
|
||||
cond do
|
||||
sorter.(entry, min) ->
|
||||
[entry | max]
|
||||
|
||||
sorter.(max, entry) ->
|
||||
[min | entry]
|
||||
|
||||
true ->
|
||||
acc
|
||||
end
|
||||
reduce_fun = fn entry, [min | max] ->
|
||||
[Kernel.min(min, entry) | Kernel.max(max, entry)]
|
||||
end
|
||||
|
||||
case reduce_by(enumerable, first_fun, reduce_fun) do
|
||||
@@ -2302,8 +2200,8 @@ defmodule Enum do
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable as calculated by the given function.
|
||||
|
||||
By default, the comparison is done with the [`<`](`</2`) sorter function,
|
||||
as the function must not return `true` for equal elements.
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -2361,7 +2259,7 @@ defmodule Enum do
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
|
||||
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
@@ -2392,7 +2290,7 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
|
||||
@doc """
|
||||
Splits the `enumerable` in two lists according to the given function `fun`.
|
||||
@@ -4196,11 +4094,6 @@ defmodule Enum do
|
||||
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
|
||||
[4, 6]
|
||||
|
||||
`zip_with/2` can be used to transpose lists of lists:
|
||||
|
||||
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
|
||||
[[1, 3], [2, 4]]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec zip_with(t, ([term] -> term)) :: [term]
|
||||
@@ -4512,9 +4405,19 @@ defmodule Enum do
|
||||
|
||||
# dedup
|
||||
|
||||
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
|
||||
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
|
||||
defp dedup_list([]), do: []
|
||||
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
|
||||
|
||||
## drop
|
||||
|
||||
@@ -5108,7 +5011,8 @@ end
|
||||
defimpl Enumerable, for: List do
|
||||
def count(list), do: {:ok, length(list)}
|
||||
|
||||
def member?(list, value), do: {:ok, :lists.member(value, list)}
|
||||
def member?([], _value), do: {:ok, false}
|
||||
def member?(_list, _value), do: {:error, __MODULE__}
|
||||
|
||||
def slice([]), do: {:ok, 0, fn _, _, _ -> [] end}
|
||||
def slice(_list), do: {:error, __MODULE__}
|
||||
@@ -5163,16 +5067,7 @@ defimpl Enumerable, for: Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
reduce =
|
||||
quote generated: true do
|
||||
reduce(
|
||||
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
|
||||
var!(acc),
|
||||
var!(fun)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(reduce) do
|
||||
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
reduce(Map.put(range, :step, step), acc, fun)
|
||||
end
|
||||
@@ -5195,12 +5090,12 @@ defimpl Enumerable, for: Range do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
def member?(first..last//step, value) when is_integer(value) and step > 0 do
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
end
|
||||
|
||||
def member?(first..last//step, value) when is_integer(value) and step < 0 do
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
def member?(first..last//step, value) when is_integer(value) do
|
||||
if step > 0 do
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
else
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
@@ -5219,17 +5114,11 @@ defimpl Enumerable, for: Range do
|
||||
end
|
||||
|
||||
def slice(first.._//step = range) do
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)}
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
|
||||
slice =
|
||||
quote generated: true do
|
||||
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
|
||||
end
|
||||
|
||||
def unquote(slice) do
|
||||
def slice(%{__struct__: Range, first: first, last: last} = range) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
slice(Map.put(range, :step, step))
|
||||
end
|
||||
|
||||
+34
-17
@@ -26,7 +26,7 @@ defmodule Exception do
|
||||
@typedoc "The exception type"
|
||||
@type t :: %{
|
||||
required(:__struct__) => module,
|
||||
required(:__exception__) => term,
|
||||
required(:__exception__) => true,
|
||||
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__: _}), do: true
|
||||
def exception?(%_{__exception__: true}), 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__: _} = exception) do
|
||||
def message(%module{__exception__: true} = 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__: _} = payload, _stacktrace), do: payload
|
||||
def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload
|
||||
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
|
||||
def normalize(_kind, payload, _stacktrace), do: payload
|
||||
|
||||
@@ -287,10 +287,10 @@ defmodule Exception do
|
||||
end
|
||||
end
|
||||
|
||||
defp map_node?({:is_map, _, [_]}), do: true
|
||||
defp map_node?(_), do: false
|
||||
defp map_key_node?({:is_map_key, _, [_, _]}), do: true
|
||||
defp map_key_node?(_), do: false
|
||||
defp is_map_node?({:is_map, _, [_]}), do: true
|
||||
defp is_map_node?(_), do: false
|
||||
defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
|
||||
defp is_map_key_node?(_), do: false
|
||||
|
||||
defp struct_validation_node?(
|
||||
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
|
||||
@@ -304,16 +304,16 @@ defmodule Exception do
|
||||
|
||||
defp struct_validation_node?(_), do: false
|
||||
|
||||
defp struct_macro?(
|
||||
defp is_struct_macro?(
|
||||
{:and, _,
|
||||
[
|
||||
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]},
|
||||
%{node: node_3 = {_, _, [{_, _, [_, arg]}]}}
|
||||
]}
|
||||
),
|
||||
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
|
||||
defp struct_macro?(
|
||||
defp is_struct_macro?(
|
||||
{:and, _,
|
||||
[
|
||||
{:and, _,
|
||||
@@ -328,12 +328,12 @@ defmodule Exception do
|
||||
%{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}}
|
||||
]}
|
||||
),
|
||||
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
|
||||
defp struct_macro?(_), do: false
|
||||
defp is_struct_macro?(_), do: false
|
||||
|
||||
defp translate_guard(guard) do
|
||||
if struct_macro?(guard) do
|
||||
if is_struct_macro?(guard) do
|
||||
undo_is_struct_guard(guard)
|
||||
else
|
||||
guard
|
||||
@@ -1388,7 +1388,6 @@ defmodule CompileError do
|
||||
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
|
||||
the error occurred in code that did not come from a file
|
||||
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
|
||||
* `:description` (`t:String.t/0`) - a description of the compile error
|
||||
|
||||
This is mostly raised by Elixir tooling when compiling and evaluating code.
|
||||
"""
|
||||
@@ -1457,6 +1456,20 @@ defmodule BadFunctionError do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadStructError do
|
||||
@moduledoc deprecated:
|
||||
"This exception is deprecated alongside the struct update syntax that raises it"
|
||||
defexception [:struct, :term]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
Exception.__format_message_with_term__(
|
||||
"expected a struct named #{inspect(exception.struct)}, got:",
|
||||
exception.term
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadMapError do
|
||||
@moduledoc """
|
||||
An exception raised when a map is expected, but something else was given.
|
||||
@@ -1930,8 +1943,8 @@ defmodule FunctionClauseError do
|
||||
|
||||
For example:
|
||||
|
||||
iex> List.duplicate(:ok, -3)
|
||||
** (FunctionClauseError) no function clause matching in List.duplicate/2
|
||||
iex> URI.parse(:wrong_argument)
|
||||
** (FunctionClauseError) no function clause matching in URI.parse/1
|
||||
|
||||
The following fields of this exception are public and can be accessed freely:
|
||||
|
||||
@@ -2591,6 +2604,10 @@ defmodule ErlangError do
|
||||
%BadFunctionError{term: term}
|
||||
end
|
||||
|
||||
def normalize({:badstruct, struct, term}, _stacktrace) do
|
||||
%BadStructError{struct: struct, term: term}
|
||||
end
|
||||
|
||||
def normalize({:badmatch, term}, _stacktrace) do
|
||||
%MatchError{term: term}
|
||||
end
|
||||
|
||||
+24
-116
@@ -400,13 +400,6 @@ defmodule File do
|
||||
|
||||
You can use `:file.format_error/1` to get a descriptive string of the error.
|
||||
|
||||
## Options (since v1.20)
|
||||
|
||||
The supported options are:
|
||||
|
||||
* `:raw` - a single atom to bypass the file server and only check
|
||||
for the file locally
|
||||
|
||||
## Examples
|
||||
|
||||
File.read("hello.txt")
|
||||
@@ -415,24 +408,15 @@ defmodule File do
|
||||
File.read("non_existing.txt")
|
||||
#=> {:error, :enoent}
|
||||
"""
|
||||
@spec read(Path.t(), [exists_option]) ::
|
||||
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
|
||||
when exists_option: :raw
|
||||
def read(path, opts \\ []) do
|
||||
:file.read_file(IO.chardata_to_string(path), opts)
|
||||
@spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
|
||||
def read(path) do
|
||||
:file.read_file(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary with the contents of the given filename,
|
||||
or raises a `File.Error` exception if an error occurs.
|
||||
|
||||
## Options (since v1.20)
|
||||
|
||||
The supported options are:
|
||||
|
||||
* `:raw` - a single atom to bypass the file server and only check
|
||||
for the file locally
|
||||
|
||||
## Examples
|
||||
|
||||
File.read!("hello.txt")
|
||||
@@ -441,9 +425,9 @@ defmodule File do
|
||||
File.read!("non_existing.txt")
|
||||
** (File.Error) could not read file "non_existing.txt": no such file or directory
|
||||
"""
|
||||
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw
|
||||
def read!(path, opts \\ []) do
|
||||
case read(path, opts) do
|
||||
@spec read!(Path.t()) :: binary
|
||||
def read!(path) do
|
||||
case read(path) do
|
||||
{:ok, binary} ->
|
||||
binary
|
||||
|
||||
@@ -710,7 +694,7 @@ defmodule File do
|
||||
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
{:error, :enoent}
|
||||
|
||||
File.touch("/tmp/a.txt", 1_544_519_753)
|
||||
File.touch("/tmp/a.txt", 1544519753)
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -749,7 +733,7 @@ defmodule File do
|
||||
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
|
||||
|
||||
File.touch!("/tmp/a.txt", 1_544_519_753)
|
||||
File.touch!("/tmp/a.txt", 1544519753)
|
||||
|
||||
"""
|
||||
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
|
||||
@@ -1103,7 +1087,7 @@ defmodule File do
|
||||
|
||||
@doc ~S"""
|
||||
Copies the contents in `source` to `destination` recursively, maintaining the
|
||||
source directory structure and regular file modes.
|
||||
source directory structure and modes.
|
||||
|
||||
If `source` is a file or a symbolic link to it, `destination` must be a path
|
||||
to an existent file, a symbolic link to one, or a path to a non-existent file.
|
||||
@@ -1114,9 +1098,7 @@ defmodule File do
|
||||
If the source is a file, it copies `source` to `destination`. If the `source`
|
||||
is a directory, it copies the contents inside source into the `destination` directory.
|
||||
|
||||
For regular files, their respective file modes are preserved in the destination.
|
||||
Directory modes are preserved only when `:preserve_directory_permissions` is `true`.
|
||||
If a file already exists in the destination, it invokes the optional `:on_conflict`
|
||||
If a file already exists in the destination, it invokes the optional `on_conflict`
|
||||
callback given as an option. See "Options" for more information.
|
||||
|
||||
This function may fail while copying files, in such cases, it will leave the
|
||||
@@ -1132,14 +1114,6 @@ defmodule File do
|
||||
explicitly disallow this behavior. If `source` is a `file` and `destination`
|
||||
is a directory, `{:error, :eisdir}` will be returned.
|
||||
|
||||
Special files such as device files, sockets, and named pipes are not copied.
|
||||
|
||||
Typical error reasons are:
|
||||
|
||||
* `:enoent` - `source` does not exist
|
||||
* `:eisdir` - `source` is a file and `destination` is a directory
|
||||
* `:einval` - `destination` is the same as or a subdirectory of `source`
|
||||
|
||||
## Options
|
||||
|
||||
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
|
||||
@@ -1153,11 +1127,6 @@ 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"
|
||||
@@ -1175,16 +1144,11 @@ defmodule File do
|
||||
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
|
||||
|
||||
File.cp_r("non_existing.txt", "copy.txt")
|
||||
#=> {:error, :enoent, "non_existing.txt"}
|
||||
|
||||
# Copying into a subdirectory of source is not allowed
|
||||
File.cp_r("src", "src/dest")
|
||||
#=> {:error, :einval, "src/dest"}
|
||||
#=> {:error, :enoent}
|
||||
"""
|
||||
@spec cp_r(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
dereference_symlinks: boolean()
|
||||
) ::
|
||||
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
|
||||
|
||||
@@ -1206,7 +1170,6 @@ 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
|
||||
@@ -1218,25 +1181,9 @@ defmodule File do
|
||||
|> IO.chardata_to_string()
|
||||
|> assert_no_null_byte!("File.cp_r/3")
|
||||
|
||||
source_parts = source |> Path.expand() |> Path.split()
|
||||
dest_parts = destination |> Path.expand() |> Path.split()
|
||||
|
||||
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
|
||||
{:error, :einval, destination}
|
||||
else
|
||||
dereference = if dereference?, do: MapSet.new(), else: nil
|
||||
|
||||
case do_cp_r(
|
||||
source,
|
||||
destination,
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_directory_permissions?,
|
||||
[]
|
||||
) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
end
|
||||
case do_cp_r(source, destination, on_conflict, dereference?, []) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1257,8 +1204,7 @@ defmodule File do
|
||||
"""
|
||||
@spec cp_r!(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
dereference_symlinks: boolean()
|
||||
) :: [binary]
|
||||
def cp_r!(source, destination, options \\ []) do
|
||||
case cp_r(source, destination, options) do
|
||||
@@ -1275,7 +1221,7 @@ defmodule File do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
|
||||
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
|
||||
case :elixir_utils.read_link_type(src) do
|
||||
{:ok, :regular} ->
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
@@ -1288,15 +1234,8 @@ defmodule File do
|
||||
|
||||
{:ok, :symlink} ->
|
||||
case :file.read_link(src) do
|
||||
{:ok, link} when dereference != nil ->
|
||||
resolved = Path.expand(link, Path.dirname(src))
|
||||
|
||||
if MapSet.member?(dereference, resolved) do
|
||||
{:error, :eloop, src}
|
||||
else
|
||||
dereference = MapSet.put(dereference, resolved)
|
||||
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
|
||||
end
|
||||
{:ok, link} when dereference? ->
|
||||
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
|
||||
|
||||
{:ok, link} ->
|
||||
do_cp_link(link, src, dest, on_conflict, acc)
|
||||
@@ -1310,35 +1249,9 @@ defmodule File do
|
||||
{:ok, files} ->
|
||||
case mkdir(dest) do
|
||||
success when success in [:ok, {:error, :eexist}] ->
|
||||
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
|
||||
Enum.reduce(files, [dest | acc], fn x, acc ->
|
||||
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
|
||||
end)
|
||||
|> case do
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
files when preserve_dir_perms? ->
|
||||
# Change the directory after writing files in case
|
||||
# it was originally read only
|
||||
case copy_file_mode(src, dest) do
|
||||
:ok -> files
|
||||
{:error, reason} -> {:error, reason, src}
|
||||
end
|
||||
|
||||
files ->
|
||||
files
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, dest}
|
||||
@@ -1349,7 +1262,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
{:ok, _} ->
|
||||
acc
|
||||
{:error, :eio, src}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, src}
|
||||
@@ -1357,7 +1270,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
|
||||
|
||||
@@ -2252,12 +2165,7 @@ defmodule File do
|
||||
def stream!(path, line_or_bytes, modes)
|
||||
|
||||
def stream!(path, modes, line_or_bytes) when is_list(modes) do
|
||||
# TODO: Remove me on Elixir 2.0
|
||||
IO.warn(
|
||||
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
|
||||
"invoke File.stream!(path, line_or_bytes, modes) instead"
|
||||
)
|
||||
|
||||
# TODO: Deprecate this on Elixir v1.20
|
||||
stream!(path, line_or_bytes, modes)
|
||||
end
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ defmodule File.Stream do
|
||||
|
||||
counter = fn device ->
|
||||
device = skip_bom_and_offset(device, raw, modes)
|
||||
count_lines(device, path, pattern, read_function(stream), 0, :empty)
|
||||
count_lines(device, path, pattern, read_function(stream), 0)
|
||||
end
|
||||
|
||||
{:ok, open!(stream, modes, counter)}
|
||||
@@ -229,28 +229,21 @@ defmodule File.Stream do
|
||||
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
||||
end
|
||||
|
||||
defp count_lines(device, path, pattern, read, count, last_byte) do
|
||||
defp count_lines(device, path, pattern, read, count) do
|
||||
case read.(device) do
|
||||
data when is_binary(data) and byte_size(data) > 0 ->
|
||||
newlines = length(:binary.matches(data, pattern))
|
||||
last = :binary.last(data)
|
||||
count_lines(device, path, pattern, read, count + newlines, last)
|
||||
|
||||
data when is_binary(data) ->
|
||||
count_lines(device, path, pattern, read, count, last_byte)
|
||||
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
|
||||
|
||||
:eof ->
|
||||
case last_byte do
|
||||
:empty -> 0
|
||||
?\n -> count
|
||||
_ -> count + 1
|
||||
end
|
||||
count
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
|
||||
|
||||
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
|
||||
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
|
||||
end
|
||||
|
||||
+121
-137
@@ -42,7 +42,7 @@ defmodule Float do
|
||||
|
||||
To learn more about floating-point arithmetic visit:
|
||||
|
||||
* [0.30000000000000004.com](https://0.30000000000000004.com/)
|
||||
* [0.30000000000000004.com](http://0.30000000000000004.com/)
|
||||
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
|
||||
|
||||
"""
|
||||
@@ -186,9 +186,10 @@ defmodule Float do
|
||||
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
|
||||
|
||||
# :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
|
||||
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
|
||||
defp parse_unsigned(rest, dot?, _, acc) do
|
||||
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
|
||||
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
|
||||
# rescue the ArgumentError here and return an error.
|
||||
defp parse_unsigned(rest, dot?, true = _e?, acc) do
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
@@ -199,6 +200,16 @@ defmodule Float do
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
defp parse_unsigned(rest, dot?, false = _e?, acc) do
|
||||
float =
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
|> :erlang.list_to_float()
|
||||
|
||||
{float, rest}
|
||||
end
|
||||
|
||||
defp add_dot(acc, true), do: acc
|
||||
defp add_dot(acc, false), do: [?0, ?. | acc]
|
||||
|
||||
@@ -344,12 +355,15 @@ 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 :erlang.round(float) * 1.0 do
|
||||
case float |> :erlang.round() |> :erlang.float() do
|
||||
zero when zero == 0.0 and float < 0.0 -> -0.0
|
||||
rounded -> rounded
|
||||
end
|
||||
@@ -363,170 +377,140 @@ 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, mode) do
|
||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||
defp round(float, precision, rounding) do
|
||||
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||
{num, count} = decompose(significant, 1)
|
||||
count = count - exp + 1023
|
||||
|
||||
cond do
|
||||
# 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)
|
||||
# Precision beyond 15 digits
|
||||
count >= 104 ->
|
||||
case rounding do
|
||||
:ceil when sign === 0 -> 1 / power_of_10(precision)
|
||||
:floor when sign === 1 -> -1 / power_of_10(precision)
|
||||
:ceil when sign === 1 -> minus_zero()
|
||||
:half_up when sign === 1 -> minus_zero()
|
||||
_ -> 0.0
|
||||
end
|
||||
|
||||
# |float| >= 2^52 — has no fractional bits, return unchanged.
|
||||
exp - 1075 >= 0 ->
|
||||
# We are asking more precision than we have
|
||||
count <= precision ->
|
||||
float
|
||||
|
||||
true ->
|
||||
mantissa = @power_of_2_to_52 ||| mantissa
|
||||
shift = 1075 - exp
|
||||
do_round(sign, mantissa, shift, precision, mode)
|
||||
# Difference in precision between float and asked precision
|
||||
# We subtract 1 because we need to calculate the remainder too
|
||||
diff = count - precision - 1
|
||||
|
||||
# Get up to latest so we calculate the remainder
|
||||
power_of_10 = power_of_10(diff)
|
||||
|
||||
# Convert the numerand to decimal base
|
||||
num = num * power_of_5(count)
|
||||
|
||||
# Move to the given precision - 1
|
||||
num = div(num, power_of_10)
|
||||
div = div(num, 10)
|
||||
num = rounding(rounding, sign, num, div)
|
||||
|
||||
# Convert back to float without loss
|
||||
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
|
||||
den = power_of_10(precision)
|
||||
boundary = den <<< 52
|
||||
|
||||
cond do
|
||||
num == 0 and sign == 1 ->
|
||||
minus_zero()
|
||||
|
||||
num == 0 ->
|
||||
0.0
|
||||
|
||||
num >= boundary ->
|
||||
{den, exp} = scale_down(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
|
||||
true ->
|
||||
{num, exp} = scale_up(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# |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)
|
||||
# TODO remove once we require Erlang/OTP 27+
|
||||
# This function tricks the compiler to avoid this bug in previous versions:
|
||||
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
|
||||
defp minus_zero, do: -0.0
|
||||
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, initial)
|
||||
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(<<1::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
|
||||
end
|
||||
|
||||
cond do
|
||||
rounded_int == 0 ->
|
||||
signed_zero(sign)
|
||||
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, last_count, acc)
|
||||
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 decompose(<<>>, _count, last_count, acc) do
|
||||
{acc, last_count}
|
||||
end
|
||||
|
||||
true ->
|
||||
bignum_to_float(sign, rounded_int, power)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
defp round_step(:half_up, _sign, quotient, remainder, half) do
|
||||
if remainder >= half, do: quotient + 1, else: quotient
|
||||
end
|
||||
defp decimal_to_float(sign, num, den, exp) do
|
||||
quo = div(num, den)
|
||||
rem = num - quo * den
|
||||
|
||||
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
|
||||
tmp =
|
||||
case den >>> 1 do
|
||||
den when rem > den -> quo + 1
|
||||
den when rem < den -> quo
|
||||
_ when (quo &&& 1) === 1 -> quo + 1
|
||||
_ -> quo
|
||||
end
|
||||
|
||||
# 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
|
||||
tmp = tmp - @power_of_2_to_52
|
||||
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
|
||||
tmp
|
||||
end
|
||||
|
||||
# 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(:floor, 1, _num, div), do: div + 1
|
||||
defp rounding(:ceil, 0, _num, div), do: 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}
|
||||
defp rounding(:half_up, _sign, num, div) do
|
||||
case rem(num, 10) do
|
||||
rem when rem < 5 -> div
|
||||
rem when rem >= 5 -> div + 1
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
defp rounding(_, _, _, div), do: div
|
||||
|
||||
Enum.reduce(0..15, 1, fn exponent, acc ->
|
||||
defp power_of_10(unquote(exponent)), do: unquote(acc)
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_10(unquote(x)), 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.
|
||||
|
||||
@@ -210,14 +210,13 @@ defmodule GenServer do
|
||||
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
|
||||
child should be restarted, defaults to `:permanent`
|
||||
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
|
||||
shut down the child, either immediately or by giving it time to shut down,
|
||||
defaults to `5_000`
|
||||
shut down the child, either immediately or by giving it time to shut down
|
||||
|
||||
For example:
|
||||
|
||||
use GenServer, restart: :transient, shutdown: 10_000
|
||||
|
||||
See the ["Child specification"](`m:Supervisor#module-child-specification`) section in the `Supervisor` module for more
|
||||
See the ["Child specification"](`m:Supervisor#module-child_spec-1-function`) section in the `Supervisor` module for more
|
||||
detailed information. The `@doc` annotation immediately preceding
|
||||
`use GenServer` will be attached to the generated `child_spec/1` function.
|
||||
|
||||
@@ -232,8 +231,6 @@ defmodule GenServer do
|
||||
a name on start via the `:name` option. Registered names are also
|
||||
automatically cleaned up on termination. The supported values are:
|
||||
|
||||
* `nil` (default) - the GenServer is not registered with a name.
|
||||
|
||||
* an atom - the GenServer is registered locally (to the current node)
|
||||
with the given name using `Process.register/2`.
|
||||
|
||||
@@ -352,41 +349,6 @@ defmodule GenServer do
|
||||
message arriving, `handle_info/2` is called with `:timeout` as the first
|
||||
argument.
|
||||
|
||||
For example:
|
||||
|
||||
defmodule Counter do
|
||||
use GenServer
|
||||
|
||||
@timeout to_timeout(second: 5)
|
||||
|
||||
@impl true
|
||||
def init(count) do
|
||||
{:ok, count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:increment, _from, count) do
|
||||
new_count = count + 1
|
||||
{:reply, new_count, new_count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:timeout, count) do
|
||||
{:stop, :normal, count}
|
||||
end
|
||||
end
|
||||
|
||||
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
|
||||
after the initialization or after the last `:increment` call:
|
||||
|
||||
{:ok, counter_pid} = GenServer.start(Counter, 50)
|
||||
GenServer.call(counter_pid, :increment)
|
||||
#=> 51
|
||||
|
||||
# After 5 seconds
|
||||
Process.alive?(counter_pid)
|
||||
#=> false
|
||||
|
||||
## When (not) to use a GenServer
|
||||
|
||||
So far, we have learned that a `GenServer` can be used as a supervised process
|
||||
@@ -526,7 +488,7 @@ defmodule GenServer do
|
||||
* [GenServer - Elixir's Getting Started Guide](genservers.md)
|
||||
* [`:gen_server` module documentation](`:gen_server`)
|
||||
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
|
||||
|
||||
"""
|
||||
|
||||
@@ -567,12 +529,9 @@ defmodule GenServer do
|
||||
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
|
||||
after a delay to attempt a restart.
|
||||
|
||||
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.
|
||||
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`.
|
||||
"""
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok, state}
|
||||
@@ -863,7 +822,7 @@ defmodule GenServer do
|
||||
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
|
||||
|
||||
@typedoc "The GenServer name"
|
||||
@type name :: nil | atom | {:global, term} | {:via, module, term}
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "Options used by the `start*` functions"
|
||||
@type options :: [option]
|
||||
|
||||
@@ -275,6 +275,8 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, opts) do
|
||||
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
|
||||
end
|
||||
@@ -689,11 +691,6 @@ 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)
|
||||
@@ -721,15 +718,7 @@ defimpl Inspect, for: Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
inspect =
|
||||
quote generated: true do
|
||||
inspect(
|
||||
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
|
||||
var!(opts)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(inspect) do
|
||||
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
inspect(Map.put(range, :step, step), opts)
|
||||
end
|
||||
|
||||
@@ -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 `200` since
|
||||
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `100` since
|
||||
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
|
||||
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
|
||||
charlists: :infer,
|
||||
custom_options: [],
|
||||
inspect_fun: &Inspect.inspect/2,
|
||||
limit: 200,
|
||||
limit: 100,
|
||||
pretty: false,
|
||||
printable_limit: 4096,
|
||||
safe: true,
|
||||
|
||||
@@ -18,45 +18,6 @@ defmodule Integer do
|
||||
|
||||
import Bitwise
|
||||
|
||||
@doc """
|
||||
Counts the number of set bits (1) in the binary representation of a non-negative `integer`.
|
||||
|
||||
This operation is known as the Hamming weight or population count.
|
||||
|
||||
Raises an `ArithmeticError` if `integer` is negative.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.popcount(0)
|
||||
0
|
||||
|
||||
iex> Integer.popcount(1)
|
||||
1
|
||||
|
||||
iex> Integer.popcount(0b10110101)
|
||||
5
|
||||
|
||||
iex> Integer.popcount(255)
|
||||
8
|
||||
|
||||
iex> Integer.popcount(0b1111111111111111)
|
||||
16
|
||||
|
||||
iex> Integer.popcount(-1)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec popcount(non_neg_integer) :: non_neg_integer
|
||||
def popcount(integer) when is_integer(integer) and integer < 0,
|
||||
do: :erlang.error(:badarith, [integer])
|
||||
|
||||
def popcount(integer) when is_integer(integer),
|
||||
do: popcount(integer, 0)
|
||||
|
||||
defp popcount(0, acc), do: acc
|
||||
defp popcount(n, acc), do: popcount(n &&& n - 1, acc + 1)
|
||||
|
||||
@doc """
|
||||
Determines if `integer` is odd.
|
||||
|
||||
@@ -211,35 +172,6 @@ defmodule Integer do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a ceiled integer division.
|
||||
|
||||
Raises an `ArithmeticError` exception if one of the arguments is not an
|
||||
integer, or when the `divisor` is `0`.
|
||||
|
||||
This function performs a *ceiled* integer division, which means that
|
||||
the result will always be rounded towards positive infinity.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.ceil_div(5, 2)
|
||||
3
|
||||
iex> Integer.ceil_div(6, -4)
|
||||
-1
|
||||
iex> Integer.ceil_div(-99, 2)
|
||||
-49
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec ceil_div(integer, neg_integer | pos_integer) :: integer
|
||||
def ceil_div(dividend, divisor) do
|
||||
if not :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
|
||||
div(dividend, divisor) + 1
|
||||
else
|
||||
div(dividend, divisor)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the ordered digits for the given `integer`.
|
||||
|
||||
@@ -297,9 +229,8 @@ defmodule Integer do
|
||||
|
||||
defp undigits([], _base, acc), do: acc
|
||||
|
||||
defp undigits([digit | _], base, _)
|
||||
when is_integer(digit) and (digit >= base or digit <= -base),
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
|
||||
defp undigits([digit | tail], base, acc) when is_integer(digit),
|
||||
do: undigits(tail, base, acc * base + digit)
|
||||
@@ -310,7 +241,7 @@ defmodule Integer do
|
||||
An optional `base` to the corresponding integer can be provided.
|
||||
If `base` is not given, 10 will be used.
|
||||
|
||||
If successful, returns a tuple in the form of `{integer, remaining_string}`.
|
||||
If successful, returns a tuple in the form of `{integer, remainder_of_binary}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
Raises an error if `base` is less than 2 or more than 36.
|
||||
@@ -329,9 +260,6 @@ defmodule Integer do
|
||||
iex> Integer.parse("three")
|
||||
:error
|
||||
|
||||
iex> Integer.parse("404 not found")
|
||||
{404, " not found"}
|
||||
|
||||
iex> Integer.parse("34", 10)
|
||||
{34, ""}
|
||||
|
||||
@@ -532,12 +460,8 @@ defmodule Integer do
|
||||
|
||||
iex> Integer.extended_gcd(10, 0)
|
||||
{10, 1, 0}
|
||||
iex> Integer.extended_gcd(-10, 0)
|
||||
{10, -1, 0}
|
||||
iex> Integer.extended_gcd(0, 10)
|
||||
{10, 0, 1}
|
||||
iex> Integer.extended_gcd(0, -10)
|
||||
{10, 0, -1}
|
||||
iex> Integer.extended_gcd(0, 0)
|
||||
{0, 0, 0}
|
||||
|
||||
@@ -545,10 +469,8 @@ defmodule Integer do
|
||||
@doc since: "1.12.0"
|
||||
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
|
||||
def extended_gcd(0, 0), do: {0, 0, 0}
|
||||
def extended_gcd(0, b) when b > 0, do: {b, 0, 1}
|
||||
def extended_gcd(0, b) when b < 0, do: {-b, 0, -1}
|
||||
def extended_gcd(a, 0) when a > 0, do: {a, 1, 0}
|
||||
def extended_gcd(a, 0) when a < 0, do: {-a, -1, 0}
|
||||
def extended_gcd(0, b), do: {b, 0, 1}
|
||||
def extended_gcd(a, 0), do: {a, 1, 0}
|
||||
|
||||
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
extended_gcd(integer2, integer1, 0, 1, 1, 0)
|
||||
|
||||
@@ -796,29 +796,6 @@ defmodule IO do
|
||||
:erlang.iolist_size(iodata)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if an IO data (the length is zero).
|
||||
|
||||
For more information about IO data, see the ["IO data"](#module-io-data)
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.iodata_empty?([])
|
||||
true
|
||||
iex> IO.iodata_empty?([""])
|
||||
true
|
||||
iex> IO.iodata_empty?([1, 2 | <<3, 4>>])
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec iodata_empty?(iodata) :: boolean
|
||||
def iodata_empty?(""), do: true
|
||||
def iodata_empty?([]), do: true
|
||||
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
|
||||
def iodata_empty?(_), do: false
|
||||
|
||||
@doc false
|
||||
def each_stream(device, line_or_codepoints) do
|
||||
case read(device, line_or_codepoints) do
|
||||
|
||||
+17
-22
@@ -8,29 +8,24 @@ defprotocol JSON.Encoder do
|
||||
If you have a struct, you can derive the implementation of this protocol
|
||||
by specifying which fields should be encoded to JSON:
|
||||
|
||||
@derive {JSON.Encoder, only: [...]}
|
||||
@derive {JSON.Encoder, only: [....]}
|
||||
defstruct ...
|
||||
|
||||
Additionally, you can exclude specific fields using the `:except` option or
|
||||
encode all fields by omitting both options entirely, but these should be used
|
||||
with caution:
|
||||
|
||||
@derive {JSON.Encoder, except: [...]}
|
||||
defstruct ...
|
||||
It is also possible to encode all fields or skip some fields via the
|
||||
`:except` option:
|
||||
|
||||
@derive JSON.Encoder
|
||||
defstruct ...
|
||||
|
||||
> #### Leaking Private Information {: .error}
|
||||
>
|
||||
> Prefer using `:only` to avoid accidentally leaking private information when
|
||||
> new fields are added. Other approaches should be used with auction.
|
||||
> The `:except` approach should be used carefully to avoid
|
||||
> accidentally leaking private information when new fields are added.
|
||||
|
||||
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
||||
to encode to JSON:
|
||||
Finally, if you don't own the struct you want to encode to JSON,
|
||||
you may use `Protocol.derive/3` placed outside of any module:
|
||||
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct)
|
||||
|
||||
"""
|
||||
@@ -67,7 +62,7 @@ defprotocol JSON.Encoder do
|
||||
|
||||
{io, _prefix} =
|
||||
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
|
||||
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:])
|
||||
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
|
||||
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
|
||||
end)
|
||||
|
||||
@@ -130,25 +125,25 @@ end
|
||||
|
||||
defimpl JSON.Encoder, for: BitString do
|
||||
def encode(value, _encoder) do
|
||||
:json.encode_binary(value)
|
||||
:elixir_json.encode_binary(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: List do
|
||||
def encode(value, encoder) do
|
||||
:json.encode_list(value, encoder)
|
||||
:elixir_json.encode_list(value, encoder)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Integer do
|
||||
def encode(value, _encoder) do
|
||||
:json.encode_integer(value)
|
||||
:elixir_json.encode_integer(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Float do
|
||||
def encode(value, _encoder) do
|
||||
:json.encode_float(value)
|
||||
:elixir_json.encode_float(value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -408,7 +403,7 @@ defmodule JSON do
|
||||
decoders = Keyword.put_new(decoders, :null, nil)
|
||||
|
||||
try do
|
||||
:json.decode(binary, acc, Map.new(decoders))
|
||||
:elixir_json.decode(binary, acc, Map.new(decoders))
|
||||
catch
|
||||
:error, :unexpected_end ->
|
||||
{:error, {:unexpected_end, byte_size(binary)}}
|
||||
@@ -528,16 +523,16 @@ defmodule JSON do
|
||||
end
|
||||
|
||||
def protocol_encode(value, _encoder) when is_binary(value),
|
||||
do: :json.encode_binary(value)
|
||||
do: :elixir_json.encode_binary(value)
|
||||
|
||||
def protocol_encode(value, _encoder) when is_integer(value),
|
||||
do: :json.encode_integer(value)
|
||||
do: :elixir_json.encode_integer(value)
|
||||
|
||||
def protocol_encode(value, _encoder) when is_float(value),
|
||||
do: :json.encode_float(value)
|
||||
do: :elixir_json.encode_float(value)
|
||||
|
||||
def protocol_encode(value, encoder) when is_list(value),
|
||||
do: :json.encode_list(value, encoder)
|
||||
do: :elixir_json.encode_list(value, encoder)
|
||||
|
||||
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
|
||||
do: JSON.Encoder.Map.encode(value, encoder)
|
||||
|
||||
+103
-129
@@ -1999,12 +1999,6 @@ defmodule Kernel do
|
||||
{:case, extra ++ meta, args}
|
||||
end
|
||||
|
||||
defp x_is_false_or_nil do
|
||||
quote generated: true do
|
||||
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Strictly boolean "or" operator.
|
||||
|
||||
@@ -2069,20 +2063,15 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp build_boolean_check(operator, check, true_clause, false_clause) do
|
||||
bools =
|
||||
quote do
|
||||
false -> unquote(false_clause)
|
||||
true -> unquote(true_clause)
|
||||
end
|
||||
|
||||
error =
|
||||
quote generated: true do
|
||||
other -> :erlang.error({:badbool, unquote(operator), other})
|
||||
end
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, operator}],
|
||||
{:case, [], [check, [do: bools ++ error]]}
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(check) do
|
||||
false -> unquote(false_clause)
|
||||
true -> unquote(true_clause)
|
||||
other -> :erlang.error({:badbool, unquote(operator), other})
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@@ -2109,10 +2098,10 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, :"!!"}],
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> false
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
@@ -2123,10 +2112,10 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, :!}],
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> true
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2752,7 +2741,7 @@ defmodule Kernel do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{__exception__: _} -> true
|
||||
%_{__exception__: true} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2764,7 +2753,8 @@ 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))
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2791,7 +2781,7 @@ defmodule Kernel do
|
||||
case unquote(name) do
|
||||
name when is_atom(name) ->
|
||||
case unquote(term) do
|
||||
%{__struct__: ^name, __exception__: _} -> true
|
||||
%{__struct__: ^name, __exception__: true} -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
@@ -2809,7 +2799,8 @@ 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))
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3533,7 +3524,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
A convenience macro that checks if the result of `expression` matches `pattern`.
|
||||
A convenience macro that checks if the right side (an expression) matches the
|
||||
left side (a pattern).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -3611,7 +3603,7 @@ defmodule Kernel do
|
||||
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
|
||||
|
||||
"""
|
||||
defmacro match?(pattern, expression) do
|
||||
defmacro match?(pattern, expr) do
|
||||
success =
|
||||
quote do
|
||||
unquote(pattern) -> true
|
||||
@@ -3622,7 +3614,7 @@ defmodule Kernel do
|
||||
_ -> false
|
||||
end
|
||||
|
||||
{:case, [], [expression, [do: success ++ failure]]}
|
||||
{:case, [], [expr, [do: success ++ failure]]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4052,10 +4044,10 @@ defmodule Kernel do
|
||||
|
||||
defp build_if(condition, do: do_clause, else: else_clause) do
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, :if}],
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
|
||||
_ -> unquote(do_clause)
|
||||
end
|
||||
end
|
||||
@@ -4103,15 +4095,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp build_unless(condition, do: do_clause, else: else_clause) do
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, :unless}],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(do_clause)
|
||||
_ -> unquote(else_clause)
|
||||
end
|
||||
end
|
||||
)
|
||||
quote do
|
||||
if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
|
||||
end
|
||||
end
|
||||
|
||||
defp build_unless(_condition, _arguments) do
|
||||
@@ -4379,10 +4365,10 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
|
||||
|
||||
annotate_case(
|
||||
[type_check: {:case, :&&}],
|
||||
[type_check: :expr],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x
|
||||
|
||||
_ ->
|
||||
@@ -4422,10 +4408,10 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "||")
|
||||
|
||||
annotate_case(
|
||||
[type_check: {:case, :||}],
|
||||
[type_check: :expr],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
unquote(right)
|
||||
|
||||
x ->
|
||||
@@ -4703,13 +4689,17 @@ defmodule Kernel do
|
||||
false
|
||||
|
||||
[] ->
|
||||
# inlined as false in erlang pass
|
||||
quote(do: :lists.member(unquote(left), []))
|
||||
quote do
|
||||
_ = unquote(left)
|
||||
false
|
||||
end
|
||||
|
||||
[head | tail] = list ->
|
||||
case in_body? do
|
||||
false -> in_list(left, head, tail, expand, list)
|
||||
true -> quote(do: :lists.member(unquote(left), unquote(right)))
|
||||
# We only expand lists in the body if they are relatively
|
||||
# short and it is made only of literal expressions.
|
||||
case not in_body? or small_literal_list?(right) do
|
||||
true -> in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
|
||||
false -> quote(do: :lists.member(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
%{} = right ->
|
||||
@@ -4721,7 +4711,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.__in__(unquote(left), unquote(right)))
|
||||
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
|
||||
|
||||
_ ->
|
||||
raise_on_invalid_args_in_2(right)
|
||||
@@ -4756,6 +4746,12 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp small_literal_list?(list) when is_list(list) and length(list) <= 32 do
|
||||
:lists.all(fn x -> is_binary(x) or is_atom(x) or is_number(x) end, list)
|
||||
end
|
||||
|
||||
defp small_literal_list?(_list), do: false
|
||||
|
||||
defp in_range(left, first, last, step) when is_integer(step) do
|
||||
in_range_literal(left, first, last, step)
|
||||
end
|
||||
@@ -4763,8 +4759,8 @@ defmodule Kernel do
|
||||
defp in_range(left, first, last, step) do
|
||||
quoted =
|
||||
quote do
|
||||
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
|
||||
unquote(generated_is_integer(last)) and
|
||||
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
|
||||
:erlang.is_integer(unquote(last)) and
|
||||
((:erlang.>(unquote(step), 0) and
|
||||
unquote(increasing_compare(left, first, last))) or
|
||||
(:erlang.<(unquote(step), 0) and
|
||||
@@ -4780,9 +4776,9 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_literal(left, first, last, step) when step > 0 do
|
||||
quoted =
|
||||
quote generated: true do
|
||||
Kernel.and(
|
||||
unquote(generated_is_integer(left)),
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
:erlang.is_integer(unquote(left)),
|
||||
unquote(increasing_compare(left, first, last))
|
||||
)
|
||||
end
|
||||
@@ -4792,9 +4788,9 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_literal(left, first, last, step) when step < 0 do
|
||||
quoted =
|
||||
quote generated: true do
|
||||
Kernel.and(
|
||||
unquote(generated_is_integer(left)),
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
:erlang.is_integer(unquote(left)),
|
||||
unquote(decreasing_compare(left, first, last))
|
||||
)
|
||||
end
|
||||
@@ -4808,28 +4804,36 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_step(quoted, left, first, step) do
|
||||
quote do
|
||||
Kernel.and(
|
||||
:erlang.andalso(
|
||||
unquote(quoted),
|
||||
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp in_list(left, head, tail, expand, right) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail])
|
||||
:lists.foldl("e(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
|
||||
defp in_list(left, head, tail, expand, right, in_body?) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
|
||||
:lists.foldl("e(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
|
||||
end
|
||||
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right) do
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
|
||||
case expand.(tail) do
|
||||
[] ->
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(head)))
|
||||
|
||||
[tail_head | tail] ->
|
||||
quote do
|
||||
Kernel.or(
|
||||
:erlang.orelse(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
unquote(in_list(left, tail_head, tail, expand, right))
|
||||
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
|
||||
)
|
||||
end
|
||||
|
||||
tail when in_body? ->
|
||||
quote do
|
||||
:erlang.orelse(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
:lists.member(unquote(left), unquote(tail))
|
||||
)
|
||||
end
|
||||
|
||||
@@ -4838,17 +4842,13 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp comp(left, right, _expand, _right) do
|
||||
defp comp(left, right, _expand, _right, _in_body?) do
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
defp generated_is_integer(arg) do
|
||||
quote generated: true, do: :erlang.is_integer(unquote(arg))
|
||||
end
|
||||
|
||||
defp increasing_compare(var, first, last) do
|
||||
quote do
|
||||
Kernel.and(
|
||||
:erlang.andalso(
|
||||
:erlang.>=(unquote(var), unquote(first)),
|
||||
:erlang."=<"(unquote(var), unquote(last))
|
||||
)
|
||||
@@ -4857,7 +4857,7 @@ defmodule Kernel do
|
||||
|
||||
defp decreasing_compare(var, first, last) do
|
||||
quote do
|
||||
Kernel.and(
|
||||
:erlang.andalso(
|
||||
:erlang."=<"(unquote(var), unquote(first)),
|
||||
:erlang.>=(unquote(var), unquote(last))
|
||||
)
|
||||
@@ -5214,11 +5214,6 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defmacro defmodule(alias, [{:do, _block}, {atom, _} | _]) when is_atom(atom) do
|
||||
raise ArgumentError,
|
||||
"unexpected reserved word at the top-level of the \"defmodule #{Macro.to_string(alias)}\" do-block: #{atom}"
|
||||
end
|
||||
|
||||
defp module_meta({_, meta, _}), do: meta
|
||||
defp module_meta(_), do: []
|
||||
|
||||
@@ -5599,8 +5594,8 @@ defmodule Kernel do
|
||||
## Types
|
||||
|
||||
It is recommended to define types for structs. By convention, such a type
|
||||
is called `t`. To define a type for a struct, the struct literal syntax is
|
||||
used:
|
||||
is called `t`. To define a struct inside a type, the struct literal syntax
|
||||
is used:
|
||||
|
||||
defmodule User do
|
||||
defstruct name: "John", age: 25
|
||||
@@ -5871,15 +5866,11 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a custom guard with the given name.
|
||||
Defines a macro suitable for use in guard expressions.
|
||||
|
||||
Once defined, custom guards can be invoked within regular code or in
|
||||
guards. The module that contains the custom guard must be required before usage.
|
||||
|
||||
Custom guards are defined by providing a valid guard expression to
|
||||
the right-hand side of `when`. `defguard` will then expand and validate
|
||||
the expressions as guards. `defguard` will raise at compile time if the
|
||||
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
|
||||
It raises at compile time if the `guard` uses expressions that aren't
|
||||
allowed in [guard clauses](patterns-and-guards.html#guards),
|
||||
and otherwise creates a macro that can be used both inside or outside guards.
|
||||
|
||||
When defining your own guards, consider the
|
||||
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
|
||||
@@ -5887,30 +5878,31 @@ defmodule Kernel do
|
||||
|
||||
## Example
|
||||
|
||||
For example, to define a guard similar to `Integer.is_even/1`, you can write:
|
||||
|
||||
defmodule Integer.Guards do
|
||||
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
|
||||
end
|
||||
|
||||
which can then be used as:
|
||||
defmodule Collatz do
|
||||
@moduledoc "Tools for working with the Collatz sequence."
|
||||
import Integer.Guards
|
||||
|
||||
require Integer.Guards
|
||||
Integer.Guards.is_even(3)
|
||||
#=> false
|
||||
@doc "Determines the number of steps `n` takes to reach `1`."
|
||||
# If this function never converges, please let me know what `n` you used.
|
||||
def converge(n) when n > 0, do: step(n, 0)
|
||||
|
||||
## Implementation details
|
||||
defp step(1, step_count) do
|
||||
step_count
|
||||
end
|
||||
|
||||
Behind the scenes, `defguard` will generate a macro which can be used
|
||||
inside and outside of guards, preserving their respective semantics.
|
||||
defp step(n, step_count) when is_even(n) do
|
||||
step(div(n, 2), step_count + 1)
|
||||
end
|
||||
|
||||
When invoked inside a guard, it behaves as if the right-hand side of
|
||||
`when` is injected as part of the guard, replacing the custom guard
|
||||
arguments by the expressions given as inputs.
|
||||
defp step(n, step_count) do
|
||||
step(3 * n + 1, step_count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
When invoked outside of a guard, it preserves regular function calling
|
||||
semantics with one caveat: all arguments are evaluated before invocation,
|
||||
except arguments which are unused, which are then never evaluated.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec defguard(Macro.t()) :: Macro.t()
|
||||
@@ -6325,15 +6317,7 @@ defmodule Kernel do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
|
||||
# The compiling process may override the callback by putting it in
|
||||
# the process dictionary.
|
||||
dbg_callback =
|
||||
case :erlang.get({:elixir, :dbg_callback}) do
|
||||
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
|
||||
value -> value
|
||||
end
|
||||
|
||||
{mod, fun, args} = dbg_callback
|
||||
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
|
||||
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
|
||||
end
|
||||
|
||||
@@ -6394,7 +6378,7 @@ defmodule Kernel do
|
||||
|
||||
With a timeout:
|
||||
|
||||
iex> to_timeout(5_400_000)
|
||||
iex> to_timeout(5400000)
|
||||
5400000
|
||||
iex> to_timeout(:infinity)
|
||||
:infinity
|
||||
@@ -6422,20 +6406,12 @@ defmodule Kernel do
|
||||
{microsecond, _precision} = duration.microsecond
|
||||
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6643,13 +6619,11 @@ defmodule Kernel do
|
||||
defmacro sigil_r(term, modifiers)
|
||||
|
||||
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
|
||||
binary = :elixir_interpolation.unescape_string(binary, ®ex_unescape_map/1)
|
||||
compile_regex(binary, options)
|
||||
end
|
||||
|
||||
defmacro sigil_r({:<<>>, meta, pieces}, options) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
|
||||
tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
compile_regex(tuple, options)
|
||||
end
|
||||
|
||||
@@ -338,7 +338,7 @@ defmodule Kernel.CLI do
|
||||
parse_argv(t, %{config | verbose_compile: true})
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
|
||||
defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
|
||||
parse_argv(t, %{config | profile: :time})
|
||||
end
|
||||
|
||||
|
||||
@@ -21,15 +21,6 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :references, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate a require
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_require(pid, meta, module, alias) do
|
||||
:gen_server.cast(pid, {:warn_require, module, meta, alias})
|
||||
module
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an alias
|
||||
must be warned if unused.
|
||||
@@ -66,11 +57,6 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.cast(pid, {:add_export, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_require(pid, module, meta) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_require, module, meta})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
|
||||
@@ -133,18 +119,12 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :unused_aliases, @timeout)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_requires(pid) do
|
||||
:gen_server.call(pid, :unused_requires, @timeout)
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init(:ok) do
|
||||
state = %{
|
||||
aliases: %{},
|
||||
imports: %{},
|
||||
requires: %{},
|
||||
references: %{},
|
||||
exports: %{},
|
||||
cache: %{},
|
||||
@@ -170,18 +150,6 @@ defmodule Kernel.LexicalTracker do
|
||||
{:reply, Enum.sort(imports), state}
|
||||
end
|
||||
|
||||
def handle_call(:unused_requires, _from, state) do
|
||||
%{references: references, aliases: aliases} = state
|
||||
|
||||
unused_requires =
|
||||
for {module, {meta, alias}} <- state.requires,
|
||||
Map.get(references, module) != :compile do
|
||||
{module, meta, alias, Map.get(aliases, alias) == :used}
|
||||
end
|
||||
|
||||
{:reply, Enum.sort(unused_requires), state}
|
||||
end
|
||||
|
||||
def handle_call(:references, _from, state) do
|
||||
{compile, runtime} = partition(Map.to_list(state.references), [], [])
|
||||
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
|
||||
@@ -277,10 +245,6 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, put_in(state.imports[module][@warn_key], true)}
|
||||
end
|
||||
|
||||
def handle_cast({:warn_require, module, meta, alias}, state) do
|
||||
{:noreply, put_in(state.requires[module], {meta, alias})}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
|
||||
@@ -25,8 +25,9 @@ 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: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
|
||||
{:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
@@ -34,8 +35,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
dest: Path.t(),
|
||||
beam_timestamp: term(),
|
||||
return_diagnostics: boolean(),
|
||||
max_concurrency: pos_integer(),
|
||||
purge_compiler_modules: boolean()
|
||||
max_concurrency: pos_integer()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
@@ -51,8 +51,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
@doc """
|
||||
Starts a task for parallel compilation.
|
||||
"""
|
||||
# TODO: Remove me on Elixir 2.0
|
||||
@deprecated "Use `pmap/2` instead"
|
||||
# TODO: Deprecate this on Elixir v1.20.
|
||||
@doc deprecated: "Use `pmap/2` instead"
|
||||
def async(fun) when is_function(fun, 0) do
|
||||
{ref, task} = inner_async(fun)
|
||||
send(task.pid, ref)
|
||||
@@ -197,11 +197,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
deprecation warnings, and type checking should run. Defaults to `true`.
|
||||
We recommend disabling it only for debugging purposes.
|
||||
|
||||
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
|
||||
each module type check, and group pass checker
|
||||
|
||||
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
|
||||
after compilation (see `Code.purge_compiler_modules/0`)
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
|
||||
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
|
||||
this information is only used to properly annotate the BEAM files before
|
||||
@@ -339,14 +336,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
defp spawn_workers(schedulers, checker, files, output, options) do
|
||||
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
|
||||
timer_ref = :erlang.send_after(threshold, self(), :threshold_check)
|
||||
|
||||
purge_compiler_modules =
|
||||
if Keyword.get(options, :purge_compiler_modules, false) do
|
||||
fn -> :elixir_code_server.cast(:purge_compiler_modules) end
|
||||
else
|
||||
fn -> :ok end
|
||||
end
|
||||
timer_ref = Process.send_after(self(), :threshold_check, threshold)
|
||||
|
||||
{outcome, state} =
|
||||
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
|
||||
@@ -363,8 +353,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
long_compilation_threshold: threshold,
|
||||
schedulers: schedulers,
|
||||
checker: checker,
|
||||
verification?: Keyword.get(options, :verification, true),
|
||||
purge_compiler_modules: purge_compiler_modules
|
||||
verification?: Keyword.get(options, :verification, true)
|
||||
})
|
||||
|
||||
Process.cancel_timer(state.timer_ref)
|
||||
@@ -459,7 +448,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
modules = write_module_binaries(result, state.output, state)
|
||||
profile(state, "after compile callback", state.after_compile)
|
||||
|
||||
{runtime_warnings, errors} =
|
||||
runtime_warnings =
|
||||
if state.verification? do
|
||||
profile(
|
||||
state,
|
||||
@@ -470,19 +459,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
|
||||
)
|
||||
else
|
||||
{[], []}
|
||||
[]
|
||||
end
|
||||
|
||||
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
|
||||
|
||||
case errors do
|
||||
[] ->
|
||||
{{:ok, modules, info}, state}
|
||||
|
||||
_ ->
|
||||
IO.puts(:stderr, "== Type checking failed with errors ==")
|
||||
{{:error, errors, info}, state}
|
||||
end
|
||||
{{:ok, modules, info}, state}
|
||||
end
|
||||
|
||||
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
|
||||
@@ -589,11 +570,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
case cycle_return do
|
||||
{:runtime, dependent_modules, extra_warnings} ->
|
||||
state.purge_compiler_modules.()
|
||||
:elixir_code_server.cast(:purge_compiler_modules)
|
||||
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
|
||||
|
||||
{:compile, [], extra_warnings} ->
|
||||
state.purge_compiler_modules.()
|
||||
:elixir_code_server.cast(:purge_compiler_modules)
|
||||
verify_modules(result, extra_warnings ++ warnings, [], state)
|
||||
|
||||
{:compile, more, extra_warnings} ->
|
||||
@@ -840,7 +821,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
timer_ref = :erlang.send_after(state.long_compilation_threshold, self(), :threshold_check)
|
||||
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
|
||||
state = %{state | timer_ref: timer_ref}
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
@@ -898,7 +879,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp return_error(warnings, errors, state, fun) do
|
||||
state.purge_compiler_modules.()
|
||||
# Also prune compiler modules in case of errors
|
||||
:elixir_code_server.cast(:purge_compiler_modules)
|
||||
|
||||
errors =
|
||||
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
|
||||
|
||||
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Let's give it a try on IEx:
|
||||
|
||||
iex> opts = %{"width" => 10, "height" => 15}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
...> {:ok, height} <- Map.fetch(opts, "height") do
|
||||
iex> opts = %{width: 10, height: 15}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
...> {:ok, width * height}
|
||||
...> end
|
||||
{:ok, 150}
|
||||
@@ -1603,13 +1603,21 @@ defmodule Kernel.SpecialForms do
|
||||
If all clauses match, the `do` block is executed, returning its result.
|
||||
Otherwise the chain is aborted and the non-matched value is returned:
|
||||
|
||||
iex> opts = %{"width" => 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
...> {:ok, height} <- Map.fetch(opts, "height") do
|
||||
iex> opts = %{width: 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
...> {:ok, width * height}
|
||||
...> end
|
||||
:error
|
||||
|
||||
Guards can be used in patterns as well:
|
||||
|
||||
iex> users = %{"melany" => "guest", "bob" => :admin}
|
||||
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
|
||||
...> {:ok, to_string(role)}
|
||||
...> end
|
||||
{:ok, "admin"}
|
||||
|
||||
As in `for/1`, variables bound inside `with/1` won't be accessible
|
||||
outside of `with/1`.
|
||||
|
||||
@@ -1653,18 +1661,22 @@ defmodule Kernel.SpecialForms do
|
||||
An `else` option can be given to modify what is being returned from
|
||||
`with` in the case of a failed match:
|
||||
|
||||
with {:ok, content} <- File.read(path),
|
||||
:ok <- File.write(path, [content, "!"]) do
|
||||
:ok
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.error("could not append ! to \#{path} with reason: \#{reason}")
|
||||
:error
|
||||
end
|
||||
iex> opts = %{width: 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
...> {:ok, width * height}
|
||||
...> else
|
||||
...> :error ->
|
||||
...> {:error, :wrong_data}
|
||||
...>
|
||||
...> _other_error ->
|
||||
...> :unexpected_error
|
||||
...> end
|
||||
{:error, :wrong_data}
|
||||
|
||||
The `else` block works like a `case`: it can have multiple clauses,
|
||||
and the first match will be used. Variables bound inside `with`
|
||||
(such as `content` in this example) are not available in the `else` block.
|
||||
and the first match will be used. Variables bound inside `with` (such as
|
||||
`width` in this example) are not available in the `else` block.
|
||||
|
||||
If an `else` block is used and there are no matching clauses, a `WithClauseError`
|
||||
exception is raised.
|
||||
@@ -1975,13 +1987,13 @@ defmodule Kernel.SpecialForms do
|
||||
While it is not possible to match against multiple patterns in a single
|
||||
clause, it's possible to match against multiple values by using guards:
|
||||
|
||||
iex> case 2 do
|
||||
...> value when value in [1, 2] ->
|
||||
iex> case :two do
|
||||
...> value when value in [:one, :two] ->
|
||||
...> "#{value} has been matched"
|
||||
...> 3 ->
|
||||
...> "3 has been matched"
|
||||
...> :three ->
|
||||
...> "three has been matched"
|
||||
...> end
|
||||
"2 has been matched"
|
||||
"two has been matched"
|
||||
"""
|
||||
defmacro case(condition, clauses), do: error!([condition, clauses])
|
||||
|
||||
@@ -2343,7 +2355,7 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro try(args), do: error!([args])
|
||||
|
||||
@doc """
|
||||
Consumes the first message matching any of the given clauses in the current
|
||||
Checks if there is a message matching any of the given clauses in the current
|
||||
process mailbox.
|
||||
|
||||
If there is no matching message, the current process waits until a matching
|
||||
|
||||
@@ -255,20 +255,9 @@ defmodule Kernel.Typespec do
|
||||
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = type_pair ->
|
||||
cond do
|
||||
# This is a built-in type since OTP 29 but it just generates a warning for now
|
||||
{name, arity} == {:record, 0} ->
|
||||
IO.warn("type #{name}/#{arity} is overriding a built-in type",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
|
||||
built_in_type?(name, arity) ->
|
||||
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
|
||||
compile_error(env, message)
|
||||
|
||||
true ->
|
||||
:ok
|
||||
if built_in_type?(name, arity) do
|
||||
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
|
||||
compile_error(env, message)
|
||||
end
|
||||
|
||||
if Map.has_key?(type_pairs, type_pair) do
|
||||
@@ -610,7 +599,8 @@ defmodule Kernel.Typespec do
|
||||
types =
|
||||
:lists.map(
|
||||
fn %{field: field} ->
|
||||
{field, Keyword.get(fields, field, quote(do: term()))}
|
||||
default_type = if field == :__exception__, do: true, else: quote(do: term())
|
||||
{field, Keyword.get(fields, field, default_type)}
|
||||
end,
|
||||
struct_info
|
||||
)
|
||||
|
||||
@@ -266,7 +266,7 @@ defmodule Kernel.Utils do
|
||||
module.exception([])
|
||||
end
|
||||
|
||||
def raise(%_{__exception__: _} = exception) do
|
||||
def raise(%_{__exception__: true} = exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -369,7 +369,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
# Prefaces `guard` with unquoted versions of `refs`.
|
||||
defp unquote_refs_once(guard, refs, %{module: module}) do
|
||||
defp unquote_refs_once(guard, refs, module) do
|
||||
{guard, used_refs} =
|
||||
Macro.postwalk(guard, %{}, fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
|
||||
+19
-29
@@ -119,23 +119,14 @@ defmodule Keyword do
|
||||
|
||||
iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
|
||||
[foo: :atom, bar: :atom, baz: :atom]
|
||||
|
||||
iex> Keyword.from_keys([], :atom)
|
||||
[]
|
||||
|
||||
iex> Keyword.from_keys(["foo"], :bar)
|
||||
** (ArgumentError) expected a list of atoms as keys, got: "foo"
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec from_keys([key], value) :: t(value)
|
||||
def from_keys(keys, value) when is_list(keys) do
|
||||
:lists.map(
|
||||
fn
|
||||
key when is_atom(key) -> {key, value}
|
||||
other -> raise ArgumentError, "expected a list of atoms as keys, got: #{inspect(other)}"
|
||||
end,
|
||||
keys
|
||||
)
|
||||
:lists.map(&{&1, value}, keys)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -270,34 +261,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, [], keyword, [])
|
||||
validate(keyword, values, [], [], [])
|
||||
end
|
||||
|
||||
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
|
||||
defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
|
||||
case find_key!(key, values1, values2) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
|
||||
:error ->
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, original, [key | bad_keys])
|
||||
validate(keyword, values1, values2, acc, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], values1, values2, original, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
|
||||
defp validate([], values1, values2, acc, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
|
||||
end
|
||||
|
||||
defp validate([], _values1, _values2, _original, bad_keys) do
|
||||
defp validate([], _values1, _values2, _acc, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
end
|
||||
|
||||
defp validate([pair | _], _values1, _values2, _original, []) do
|
||||
defp validate([pair | _], _values1, _values2, _acc, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
@@ -446,7 +437,7 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for `key` and updates it in one pass, deleting duplicate keys.
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
|
||||
The `fun` argument receives the value of `key` (or `nil` if `key`
|
||||
is not present) and must return a two-element tuple: the current value
|
||||
@@ -492,7 +483,7 @@ defmodule Keyword do
|
||||
defp get_and_update([{key, current} | t], acc, key, fun) do
|
||||
case fun.(current) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
{get, :lists.reverse(acc, [{key, value} | t])}
|
||||
|
||||
:pop ->
|
||||
{current, :lists.reverse(acc, t)}
|
||||
@@ -518,8 +509,7 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for `key` and updates it in one pass, deleting duplicate keys,
|
||||
raising if `key` can't be found in `keywords`.
|
||||
Gets the value under `key` and updates it. Raises if there is no `key`.
|
||||
|
||||
The `fun` argument receives the value under `key` and must return a
|
||||
two-element tuple: the current value (the retrieved value, which can be
|
||||
@@ -555,21 +545,21 @@ defmodule Keyword do
|
||||
get_and_update!(keywords, key, fun, [])
|
||||
end
|
||||
|
||||
defp get_and_update!([{key, value} | t], key, fun, acc) do
|
||||
defp get_and_update!([{key, value} | keywords], key, fun, acc) do
|
||||
case fun.(value) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
|
||||
|
||||
:pop ->
|
||||
{value, :lists.reverse(acc, t)}
|
||||
{value, :lists.reverse(acc, keywords)}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update!([{_, _} = h | t], key, fun, acc) do
|
||||
get_and_update!(t, key, fun, [h | acc])
|
||||
defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
|
||||
get_and_update!(keywords, key, fun, [e | acc])
|
||||
end
|
||||
|
||||
defp get_and_update!([], key, _fun, acc) when is_atom(key) do
|
||||
@@ -965,7 +955,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 `===/2`,
|
||||
Comparison between values is done with `===/3`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Keyword.equal?([a: 1.0], [a: 1])
|
||||
|
||||
+3
-59
@@ -187,10 +187,9 @@ defmodule List do
|
||||
"""
|
||||
@spec duplicate(any, 0) :: []
|
||||
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
|
||||
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
|
||||
|
||||
defp duplicate(0, _elem, acc), do: acc
|
||||
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
|
||||
def duplicate(elem, n) do
|
||||
:lists.duplicate(n, elem)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
@@ -298,33 +297,6 @@ defmodule List do
|
||||
def first([], default), do: default
|
||||
def first([head | _], _default), do: head
|
||||
|
||||
@doc """
|
||||
Returns the first element in `list`.
|
||||
|
||||
If `list` is empty, an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.first!([1])
|
||||
1
|
||||
|
||||
iex> List.first!([1, 2, 3])
|
||||
1
|
||||
|
||||
iex> List.first!([])
|
||||
** (ArgumentError) attempted to get the first element of an empty list
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec first!([elem, ...]) :: elem when elem: var
|
||||
def first!(list)
|
||||
|
||||
def first!([head | _]), do: head
|
||||
|
||||
def first!([]) do
|
||||
raise ArgumentError, "attempted to get the first element of an empty list"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list` or `default` if `list` is empty.
|
||||
|
||||
@@ -353,34 +325,6 @@ defmodule List do
|
||||
def last([head], _default), do: head
|
||||
def last([_ | tail], default), do: last(tail, default)
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list`.
|
||||
|
||||
If `list` is empty, an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.last!([1])
|
||||
1
|
||||
|
||||
iex> List.last!([1, 2, 3])
|
||||
3
|
||||
|
||||
iex> List.last!([])
|
||||
** (ArgumentError) attempted to get the last element of an empty list
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec last!([elem, ...]) :: elem when elem: var
|
||||
def last!(list)
|
||||
|
||||
def last!([head]), do: head
|
||||
def last!([_ | tail]), do: last!(tail)
|
||||
|
||||
def last!([]) do
|
||||
raise ArgumentError, "attempted to get the last element of an empty list"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns the first tuple
|
||||
where the element at `position` in the tuple matches the
|
||||
|
||||
@@ -19,6 +19,12 @@ defprotocol List.Chars do
|
||||
"""
|
||||
@spec to_charlist(t) :: charlist
|
||||
def to_charlist(term)
|
||||
|
||||
@doc false
|
||||
@deprecated "Use List.Chars.to_charlist/1 instead"
|
||||
Kernel.def to_char_list(term) do
|
||||
__MODULE__.to_charlist(term)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
|
||||
+42
-42
@@ -508,11 +508,13 @@ defmodule Macro do
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.unique_var/2`.
|
||||
|
||||
The second argument is generally the macro caller's module.
|
||||
|
||||
## Examples
|
||||
|
||||
[var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)
|
||||
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
|
||||
iex> {:arg1, [counter: c1], __MODULE__} = var1
|
||||
iex> {:arg2, [counter: c2], __MODULE__} = var2
|
||||
iex> is_integer(c1) and is_integer(c2)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@@ -568,11 +570,11 @@ defmodule Macro do
|
||||
generate another variable, with its own unique counter.
|
||||
See `var/2` for an alternative.
|
||||
|
||||
The second argument is generally the macro caller's module.
|
||||
|
||||
## Examples
|
||||
|
||||
var = Macro.unique_var(:foo, __CALLER__.module)
|
||||
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
|
||||
iex> is_integer(c)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@@ -971,15 +973,9 @@ 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, :hard, env) do
|
||||
{:ok, info} ->
|
||||
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
|
||||
info
|
||||
|
||||
{:error, desc} ->
|
||||
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
|
||||
{:ok, info} -> info
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2196,7 +2192,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(), acc} when acc: term()
|
||||
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
|
||||
def expand_literals(ast, acc, fun)
|
||||
|
||||
def expand_literals({:__aliases__, meta, args}, acc, fun) do
|
||||
@@ -2726,29 +2722,34 @@ defmodule Macro do
|
||||
# Pipelines.
|
||||
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
|
||||
value_var = unique_var(:value, __MODULE__)
|
||||
values_acc_var = unique_var(:values, __MODULE__)
|
||||
|
||||
[start_ast | rest_asts] = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
||||
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)})
|
||||
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
||||
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
|
||||
|
||||
first_entry =
|
||||
initial_acc =
|
||||
quote do
|
||||
unquote(value_var) = unquote(start_ast)
|
||||
{:multi_value, unquote(escape(start_ast)), unquote(value_var)}
|
||||
unquote(values_acc_var) = [unquote(value_var)]
|
||||
end
|
||||
|
||||
len = length(piped_rest_asts)
|
||||
values_ast =
|
||||
for step_ast <- rest_asts, reduce: initial_acc do
|
||||
ast_acc ->
|
||||
quote do
|
||||
unquote(ast_acc)
|
||||
unquote(value_var) = unquote(step_ast)
|
||||
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
|
||||
end
|
||||
end
|
||||
|
||||
pipe_entries =
|
||||
Enum.with_index(piped_rest_asts, fn {original_ast, step_ast}, i ->
|
||||
tag = if i + 1 == len, do: :pipe_end, else: :pipe
|
||||
[
|
||||
quote do
|
||||
unquote(values_ast)
|
||||
|
||||
quote do
|
||||
unquote(value_var) = unquote(step_ast)
|
||||
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)}
|
||||
end
|
||||
end)
|
||||
|
||||
[first_entry | pipe_entries]
|
||||
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
|
||||
@@ -2968,19 +2969,18 @@ defmodule Macro do
|
||||
result
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do
|
||||
formatted = [
|
||||
[:faint, "|> ", :reset],
|
||||
dbg_format_ast_with_value_no_newline(code_ast, value, options)
|
||||
]
|
||||
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
|
||||
result = List.last(values)
|
||||
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
|
||||
[{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
|
||||
first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
|
||||
|
||||
{formatted, value}
|
||||
end
|
||||
rest_formatted =
|
||||
Enum.map(asts_with_values, fn {code_ast, value} ->
|
||||
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
|
||||
end)
|
||||
|
||||
defp dbg_format_ast_to_debug({:pipe_end, code_ast, value}, options) do
|
||||
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
|
||||
|
||||
{[formatted, ?\n], value}
|
||||
{[first_formatted | rest_formatted], result}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
|
||||
|
||||
@@ -300,7 +300,7 @@ defmodule Macro.Env do
|
||||
|
||||
iex> Macro.Env.required?(__ENV__, Integer)
|
||||
false
|
||||
iex> require Integer, warn: false
|
||||
iex> require Integer
|
||||
iex> Macro.Env.required?(__ENV__, Integer)
|
||||
true
|
||||
|
||||
@@ -367,8 +367,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
|
||||
{:ok, t} | {:error, String.t()}
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
|
||||
def define_require(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
@@ -658,7 +657,7 @@ defmodule Macro.Env do
|
||||
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
|
||||
end
|
||||
|
||||
quoted = expander.(:elixir_dispatch.stop_generated(args), env)
|
||||
quoted = expander.(args, env)
|
||||
next = :elixir_module.next_counter(env.module)
|
||||
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
|
||||
end
|
||||
|
||||
+54
-105
@@ -287,13 +287,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Fetches the value for a specific `key` in the given `map`.
|
||||
|
||||
If `map` contains the given `key` then its value is returned
|
||||
in the shape of `{:ok, value}`. If `map` doesn't contain `key`,
|
||||
`:error` is returned.
|
||||
|
||||
If the type system can verify `:error` is always returned
|
||||
(which means key is never available in the map), it will emit
|
||||
an error.
|
||||
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
|
||||
If `map` doesn't contain `key`, `:error` is returned.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -301,7 +296,7 @@ defmodule Map do
|
||||
|
||||
iex> Map.fetch(%{a: 1}, :a)
|
||||
{:ok, 1}
|
||||
iex> Map.fetch(%{"foo" => "bar"}, "unknown")
|
||||
iex> Map.fetch(%{a: 1}, :b)
|
||||
:error
|
||||
|
||||
"""
|
||||
@@ -312,11 +307,8 @@ defmodule Map do
|
||||
Fetches the value for a specific `key` in the given `map`, erroring out if
|
||||
`map` doesn't contain `key`.
|
||||
|
||||
The exclamation mark (`!`) implies this function can raise a `KeyError`
|
||||
exception at runtime if `map` doesn't contain `key`. If the type system
|
||||
can verify this function will always raise (which means the key is never
|
||||
available), then it will emit a warning at compile-time. See the "Type
|
||||
checking" section below.
|
||||
If `map` contains `key`, the corresponding value is returned. If
|
||||
`map` doesn't contain `key`, a `KeyError` exception is raised.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -325,54 +317,11 @@ defmodule Map do
|
||||
iex> Map.fetch!(%{a: 1}, :a)
|
||||
1
|
||||
|
||||
When the key is missing, an exception is raised:
|
||||
|
||||
Map.fetch!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
## Type checking
|
||||
|
||||
The compiler will emit a warning if it can verify that
|
||||
none of the keys given are available in the map.
|
||||
|
||||
When the key is an atom, because only single key is given,
|
||||
a warning will be emitted in case the type system proves
|
||||
the key is not present.
|
||||
|
||||
However, this behaviour matters when the type of the key
|
||||
represents multiple values. For example:
|
||||
|
||||
key = returns_foo_or_bar() #=> :foo or :bar
|
||||
Map.fetch!(%{foo: 123}, key)
|
||||
|
||||
Although the key can be `:foo` or `:bar`, there is no
|
||||
warning emitted, as `:foo` will succeed. This is by design:
|
||||
the exclamation mark in Elixir denotes precisely that a
|
||||
runtime exception may be raised.
|
||||
|
||||
In case you are looking up multiple keys and you don't know
|
||||
if they may be present, you can use `Map.fetch/2` instead
|
||||
and deal with the error case accordingly:
|
||||
|
||||
case Map.fetch(%{foo: 123}, key) do
|
||||
{:ok, value} -> ...
|
||||
:error -> ...
|
||||
end
|
||||
|
||||
Both `Map.fetch!/2` and `Map.fetch/2` will emit a warning if
|
||||
it proves that both `:foo` or `:bar` are absent in the map.
|
||||
|
||||
Alternatively, if you want to statically prove that all of keys
|
||||
are in the map, you can match on the possible values and access
|
||||
them directly:
|
||||
|
||||
case returns_foo_or_bar() do
|
||||
:foo -> map.foo
|
||||
:bar -> map.bar
|
||||
end
|
||||
"""
|
||||
@spec fetch!(map, key) :: value
|
||||
def fetch!(map, key), do: :maps.get(key, map)
|
||||
def fetch!(map, key) do
|
||||
:maps.get(key, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` unless the entry `key`
|
||||
@@ -408,8 +357,8 @@ defmodule Map do
|
||||
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace(%{"a" => 1}, "b", 2)
|
||||
%{"a" => 1}
|
||||
iex> Map.replace(%{a: 1}, :b, 2)
|
||||
%{a: 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@@ -430,11 +379,7 @@ defmodule Map do
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
|
||||
The exclamation mark (`!`) implies this function can raise a `KeyError`
|
||||
exception at runtime if `map` doesn't contain `key`. If the type system
|
||||
can verify this function will always raise (which means the key is never
|
||||
available), then it will emit a warning at compile-time. See the "Type
|
||||
checking" section in `Map.fetch!/2` for more information.
|
||||
If `key` is not present in `map`, a `KeyError` exception is raised.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -443,8 +388,8 @@ defmodule Map do
|
||||
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar")
|
||||
** (KeyError) key "unknown" not found in:
|
||||
iex> Map.replace!(%{a: 1}, :b, 2)
|
||||
** (KeyError) key :b not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -467,8 +412,8 @@ defmodule Map do
|
||||
iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end)
|
||||
%{a: 4, b: 2}
|
||||
|
||||
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end)
|
||||
%{"a" => 1, "b" => 2}
|
||||
iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
|
||||
%{a: 1, b: 2}
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@@ -547,8 +492,6 @@ defmodule Map do
|
||||
:erlang.error({:badmap, non_map})
|
||||
end
|
||||
|
||||
defp take([], _map, []), do: %{}
|
||||
|
||||
defp take([], _map, acc) do
|
||||
:maps.from_list(acc)
|
||||
end
|
||||
@@ -573,13 +516,15 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.get(%{"a" => 1}, "a")
|
||||
1
|
||||
iex> Map.get(%{"a" => 1}, "b")
|
||||
iex> Map.get(%{}, :a)
|
||||
nil
|
||||
iex> Map.get(%{"a" => 1}, "b", 3)
|
||||
iex> Map.get(%{a: 1}, :a)
|
||||
1
|
||||
iex> Map.get(%{a: 1}, :b)
|
||||
nil
|
||||
iex> Map.get(%{a: 1}, :b, 3)
|
||||
3
|
||||
iex> Map.get(%{"a" => nil}, "a", 1)
|
||||
iex> Map.get(%{a: nil}, :a, 1)
|
||||
nil
|
||||
|
||||
"""
|
||||
@@ -608,11 +553,15 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.get_lazy(%{a: 1}, :a, fn -> :expensive_value end)
|
||||
iex> map = %{a: 1}
|
||||
iex> fun = fn ->
|
||||
...> # some expensive operation here
|
||||
...> 13
|
||||
...> end
|
||||
iex> Map.get_lazy(map, :a, fun)
|
||||
1
|
||||
|
||||
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
|
||||
:expensive_value
|
||||
iex> Map.get_lazy(map, :b, fun)
|
||||
13
|
||||
|
||||
"""
|
||||
@spec get_lazy(map, key, (-> value)) :: value
|
||||
@@ -751,10 +700,10 @@ defmodule Map do
|
||||
|
||||
iex> Map.pop(%{a: 1}, :a)
|
||||
{1, %{}}
|
||||
iex> Map.pop(%{"a" => 1}, "b")
|
||||
{nil, %{"a" => 1}}
|
||||
iex> Map.pop(%{"a" => 1}, "b", 3)
|
||||
{3, %{"a" => 1}}
|
||||
iex> Map.pop(%{a: 1}, :b)
|
||||
{nil, %{a: 1}}
|
||||
iex> Map.pop(%{a: 1}, :b, 3)
|
||||
{3, %{a: 1}}
|
||||
|
||||
"""
|
||||
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
|
||||
@@ -777,8 +726,8 @@ defmodule Map do
|
||||
{1, %{}}
|
||||
iex> Map.pop!(%{a: 1, b: 2}, :a)
|
||||
{1, %{b: 2}}
|
||||
iex> Map.pop!(%{"a" => 1}, "b")
|
||||
** (KeyError) key "b" not found in:
|
||||
iex> Map.pop!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -804,11 +753,15 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.pop_lazy(%{a: 1}, :a, fn -> :expensive_value end)
|
||||
iex> map = %{a: 1}
|
||||
iex> fun = fn ->
|
||||
...> # some expensive operation here
|
||||
...> 13
|
||||
...> end
|
||||
iex> Map.pop_lazy(map, :a, fun)
|
||||
{1, %{}}
|
||||
|
||||
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
|
||||
{:expensive_value, %{"a" => 1}}
|
||||
iex> Map.pop_lazy(map, :b, fun)
|
||||
{13, %{a: 1}}
|
||||
|
||||
"""
|
||||
@spec pop_lazy(map, key, (-> value)) :: {value, map}
|
||||
@@ -960,8 +913,8 @@ defmodule Map do
|
||||
iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
|
||||
%{a: 2}
|
||||
|
||||
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2))
|
||||
** (KeyError) key "b" not found in:
|
||||
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
|
||||
** (KeyError) key :b not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -1080,7 +1033,6 @@ defmodule Map do
|
||||
#=> %{name: "john"}
|
||||
|
||||
"""
|
||||
# TODO: implement this using row polymorphism
|
||||
@spec from_struct(atom | struct) :: map
|
||||
def from_struct(struct) when is_atom(struct) do
|
||||
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
|
||||
@@ -1109,7 +1061,7 @@ defmodule Map do
|
||||
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
|
||||
false
|
||||
|
||||
Comparison between keys and values is done with `===/2`,
|
||||
Comparison between keys and values is done with `===/3`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Map.equal?(%{a: 1.0}, %{a: 1})
|
||||
@@ -1117,7 +1069,11 @@ defmodule Map do
|
||||
|
||||
"""
|
||||
@spec equal?(map, map) :: boolean
|
||||
def equal?(map1, map2)
|
||||
|
||||
def equal?(%{} = map1, %{} = map2), do: map1 === map2
|
||||
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
|
||||
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Kernel.map_size/1 instead"
|
||||
@@ -1138,9 +1094,9 @@ defmodule Map do
|
||||
> #### Performance considerations {: .tip}
|
||||
>
|
||||
> If you find yourself doing multiple calls to `Map.filter/2`
|
||||
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
|
||||
> a map at the end using `Map.new/1` or `Map.new/2`.
|
||||
> and `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
|
||||
> a map at the end using `Map.new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1172,13 +1128,6 @@ defmodule Map do
|
||||
|
||||
See also `filter/2`.
|
||||
|
||||
> #### Performance considerations {: .tip}
|
||||
>
|
||||
> If you find yourself doing multiple calls to `Map.filter/2`
|
||||
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
|
||||
> a map at the end using `Map.new/1` or `Map.new/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
|
||||
|
||||
@@ -55,11 +55,7 @@ defmodule MapSet do
|
||||
|
||||
@type value :: term
|
||||
|
||||
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
|
||||
# either via module attributes or by the compiler.
|
||||
# Defaulting to a broad `term()` type to prevent opaqueness violations.
|
||||
@typep internal(_value) :: term()
|
||||
|
||||
@opaque internal(value) :: :sets.set(value)
|
||||
@type t(value) :: %__MODULE__{map: internal(value)}
|
||||
@type t :: t(term)
|
||||
|
||||
|
||||
+36
-46
@@ -656,8 +656,7 @@ defmodule Module do
|
||||
@spec module_info(:attributes) :: keyword()
|
||||
@spec module_info(:compile) :: keyword()
|
||||
@spec module_info(:md5) :: binary()
|
||||
@spec module_info(:nifs) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:nifs) :: module()
|
||||
@spec module_info(:exports) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:functions) :: [function_info]
|
||||
@@ -680,7 +679,7 @@ defmodule Module do
|
||||
information. For documentation, see `c:Module.__info__/1`.
|
||||
'''
|
||||
|
||||
@type definition :: {function_name :: atom, arity}
|
||||
@type definition :: {atom, arity}
|
||||
@type def_kind :: :def | :defp | :defmacro | :defmacrop
|
||||
|
||||
@type create_opts :: [
|
||||
@@ -959,7 +958,7 @@ defmodule Module do
|
||||
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -985,7 +984,7 @@ defmodule Module do
|
||||
It handles binaries and atoms. If one of the aliases
|
||||
is nil, it is discarded.
|
||||
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1016,7 +1015,7 @@ defmodule Module do
|
||||
If the alias was not referenced yet, fails with `ArgumentError`.
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1040,7 +1039,7 @@ defmodule Module do
|
||||
If the alias was not referenced yet, fails with `ArgumentError`.
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1250,18 +1249,17 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec defines?(module, definition) :: boolean
|
||||
def defines?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
def defines?(module, {name, arity} = tuple)
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
|
||||
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
|
||||
:ets.member(set, {:def, definition})
|
||||
:ets.member(set, {:def, tuple})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the module defines a function or macro of the
|
||||
given kind.
|
||||
given `kind`.
|
||||
|
||||
`def_kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
|
||||
`kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
|
||||
|
||||
This function can only be used on modules that have not yet been compiled.
|
||||
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
|
||||
@@ -1277,13 +1275,12 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec defines?(module, definition, def_kind) :: boolean
|
||||
def defines?(module, {function_name, arity} = definition, def_kind)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 and
|
||||
def defines?(module, {name, arity} = tuple, def_kind)
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
|
||||
def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
|
||||
|
||||
case :ets.lookup(set, {:def, definition}) do
|
||||
case :ets.lookup(set, {:def, tuple}) do
|
||||
[{_, ^def_kind, _, _, _, _}] -> true
|
||||
_ -> false
|
||||
end
|
||||
@@ -1296,9 +1293,7 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec defines_type?(module, definition) :: boolean
|
||||
def defines_type?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
def defines_type?(module, definition) when is_atom(module) do
|
||||
Kernel.Typespec.defines_type?(module, definition)
|
||||
end
|
||||
|
||||
@@ -1311,9 +1306,7 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec spec_to_callback(module, definition) :: boolean
|
||||
def spec_to_callback(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
def spec_to_callback(module, definition) do
|
||||
Kernel.Typespec.spec_to_callback(module, definition)
|
||||
end
|
||||
|
||||
@@ -1346,7 +1339,7 @@ defmodule Module do
|
||||
@doc """
|
||||
Returns all overridable definitions in `module`.
|
||||
|
||||
Note a definition is included even if it was already overridden.
|
||||
Note a definition is included even if it was was already overridden.
|
||||
You can use `defines?/2` to see if a definition exists or one is pending.
|
||||
|
||||
This function can only be used on modules that have not yet been compiled.
|
||||
@@ -1365,7 +1358,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec overridables_in(module) :: [definition]
|
||||
@spec overridables_in(module) :: [atom]
|
||||
def overridables_in(module) when is_atom(module) do
|
||||
assert_not_compiled!(__ENV__.function, module, :all)
|
||||
:elixir_overridable.overridables_for(module)
|
||||
@@ -1414,10 +1407,10 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec definitions_in(module, def_kind) :: [definition]
|
||||
def definitions_in(module, def_kind)
|
||||
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
def definitions_in(module, kind)
|
||||
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
|
||||
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1448,17 +1441,16 @@ defmodule Module do
|
||||
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
|
||||
| nil
|
||||
@doc since: "1.12.0"
|
||||
def get_definition(module, {function_name, arity} = _definition, options \\ [])
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 and is_list(options) do
|
||||
def get_definition(module, {name, arity}, options \\ [])
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
|
||||
{set, bag} = data_tables_for!(module, __ENV__.function, "")
|
||||
|
||||
case :ets.lookup(set, {:def, {function_name, arity}}) do
|
||||
case :ets.lookup(set, {:def, {name, arity}}) do
|
||||
[{_key, kind, meta, _, _, _}] ->
|
||||
clauses =
|
||||
if options[:skip_clauses],
|
||||
do: [],
|
||||
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
|
||||
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
|
||||
|
||||
{:v1, kind, meta, clauses}
|
||||
|
||||
@@ -1475,11 +1467,10 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec delete_definition(module, definition) :: boolean()
|
||||
def delete_definition(module, {function_name, arity} = _definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
def delete_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
assert_not_compiled!(__ENV__.function, module, :writeable)
|
||||
:elixir_def.take_definition(module, {function_name, arity}) != false
|
||||
:elixir_def.take_definition(module, {name, arity}) != false
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1495,20 +1486,20 @@ defmodule Module do
|
||||
given.
|
||||
"""
|
||||
@spec make_overridable(module, [definition]) :: :ok
|
||||
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do
|
||||
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
|
||||
assert_not_compiled!(__ENV__.function, module, :writeable)
|
||||
|
||||
func = fn
|
||||
{function_name, arity} = definition
|
||||
{function_name, arity} = tuple
|
||||
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
|
||||
case :elixir_def.take_definition(module, definition) do
|
||||
case :elixir_def.take_definition(module, tuple) do
|
||||
false ->
|
||||
raise ArgumentError,
|
||||
"cannot make function #{function_name}/#{arity} " <>
|
||||
"overridable because it was not defined"
|
||||
|
||||
clause ->
|
||||
:elixir_overridable.record_overridable(module, definition, clause)
|
||||
:elixir_overridable.record_overridable(module, tuple, clause)
|
||||
end
|
||||
|
||||
other ->
|
||||
@@ -1517,7 +1508,7 @@ defmodule Module do
|
||||
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
:lists.foreach(func, definitions)
|
||||
:lists.foreach(func, tuples)
|
||||
end
|
||||
|
||||
@spec make_overridable(module, module) :: :ok
|
||||
@@ -1575,10 +1566,9 @@ defmodule Module do
|
||||
exists or one is pending.
|
||||
"""
|
||||
@spec overridable?(module, definition) :: boolean
|
||||
def overridable?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
:elixir_overridable.overridable_for(module, definition) != :not_overridable
|
||||
def overridable?(module, {function_name, arity} = tuple)
|
||||
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 do
|
||||
:elixir_overridable.overridable_for(module, tuple) != :not_overridable
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -181,16 +181,8 @@ defmodule Module.Behaviour do
|
||||
behaviour not in behaviours ->
|
||||
{:error, {:behaviour_not_declared, behaviour}}
|
||||
|
||||
not Code.ensure_loaded?(behaviour) ->
|
||||
# Module does not exist, but we have already warned about it.
|
||||
{:ok, []}
|
||||
|
||||
not behaviour_defined?(callbacks, behaviour) ->
|
||||
# Module does not define behaviour, but we have already warned about it.
|
||||
{:ok, []}
|
||||
|
||||
true ->
|
||||
{:error, {:callback_not_defined, behaviour, callbacks}}
|
||||
{:error, {:behaviour_not_defined, behaviour, callbacks}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -223,18 +215,6 @@ defmodule Module.Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
# Determines whether there is at least one callback defined for the given behaviour.
|
||||
# If not, that means that the behaviour has not been defined.
|
||||
defp behaviour_defined?(callbacks, behaviour) do
|
||||
callbacks
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.any?(fn
|
||||
{_kind, ^behaviour, _optional?} -> true
|
||||
{_kind, _behaviour, _optional?} -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
when map_size(callbacks) == 0 do
|
||||
context
|
||||
@@ -410,17 +390,13 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
behaviour_string = inspect(behaviour)
|
||||
|
||||
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
[
|
||||
"got \"@impl ",
|
||||
behaviour_string,
|
||||
inspect(behaviour),
|
||||
"\" for ",
|
||||
format_definition(kind, callback),
|
||||
" but ",
|
||||
behaviour_string,
|
||||
" does not specify such callback",
|
||||
" but this behaviour does not specify such callback",
|
||||
known_callbacks(callbacks)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -10,7 +10,6 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type error() :: term()
|
||||
@type mode() :: :erlang | :elixir | :protocol
|
||||
|
||||
@typedoc """
|
||||
@@ -20,7 +19,6 @@ defmodule Module.ParallelChecker do
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {:profile, :time}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@@ -68,37 +66,17 @@ defmodule Module.ParallelChecker do
|
||||
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
|
||||
# Protocols may have been consolidated. So if we know their beam location,
|
||||
# we discard their module map on purpose and start from file.
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?)
|
||||
else
|
||||
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures)
|
||||
ref = make_ref()
|
||||
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
|
||||
register_cache_and_checker(checker, mode, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
|
||||
spawn(fn ->
|
||||
mon_ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{^ref, :check, profile} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
Process.link(pid)
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
|
||||
send(pid, {__MODULE__, module, warnings, errors})
|
||||
send(checker, {__MODULE__, :checked, ref})
|
||||
|
||||
{:DOWN, ^mon_ref, _, _, _} ->
|
||||
:ok
|
||||
info =
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
List.to_string(beam_location)
|
||||
else
|
||||
cache_from_module_map(table, module_map, signatures)
|
||||
end
|
||||
end)
|
||||
|
||||
inner_spawn(pid, checker, table, module, info, log?)
|
||||
end
|
||||
|
||||
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
|
||||
defp inner_spawn(pid, checker, table, module, info, log?) do
|
||||
ref = make_ref()
|
||||
|
||||
spawned =
|
||||
@@ -109,28 +87,48 @@ defmodule Module.ParallelChecker do
|
||||
{^ref, :cache} ->
|
||||
Process.link(pid)
|
||||
|
||||
location =
|
||||
case :code.which(module) do
|
||||
[_ | _] = path -> path
|
||||
_ -> info
|
||||
{mode, module_tuple} =
|
||||
cond do
|
||||
is_binary(info) ->
|
||||
location =
|
||||
case :code.which(module) do
|
||||
[_ | _] = path -> path
|
||||
_ -> info
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok,
|
||||
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
|
||||
else
|
||||
_ -> {:not_found, nil}
|
||||
end
|
||||
|
||||
is_tuple(info) ->
|
||||
info
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(exck) do
|
||||
mode = cache_chunk(table, module, contents)
|
||||
module_tuple = module_map_to_module_tuple(module_map)
|
||||
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
|
||||
:ets.insert(table, {module, mode})
|
||||
send(checker, {__MODULE__, :cached, module, spawned, ref})
|
||||
else
|
||||
_ ->
|
||||
# Nothing to check, so we notify everyone we are done
|
||||
:ets.insert(table, {module, :uncached})
|
||||
send(checker, {__MODULE__, :cached, module, nil, ref})
|
||||
send(pid, {__MODULE__, module, [], []})
|
||||
# We only make the module available now, so they are not visible during inference
|
||||
:ets.insert(table, {module, mode})
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
{^ref, :check} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
warnings =
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
send(pid, {__MODULE__, module, warnings})
|
||||
send(checker, {__MODULE__, :done, module})
|
||||
end
|
||||
|
||||
{:DOWN, ^mon_ref, _, _, _} ->
|
||||
@@ -138,7 +136,7 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end)
|
||||
|
||||
register_cache(checker, module, spawned, ref)
|
||||
register(checker, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -186,47 +184,50 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
Returns the updated list of warnings from the verification.
|
||||
"""
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]}
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
|
||||
def verify({checker, table}, runtime_files) do
|
||||
value = :erlang.get(:elixir_code_diagnostics)
|
||||
log? = not match?({_, false}, value)
|
||||
|
||||
for {module, file} <- runtime_files do
|
||||
spawn_and_register_cache(self(), checker, table, module, file, log?)
|
||||
inner_spawn(self(), checker, table, module, file, log?)
|
||||
end
|
||||
|
||||
count = :gen_server.call(checker, :start, :infinity)
|
||||
{warnings, errors} = collect_results(count, [], [])
|
||||
diagnostics = collect_results(count, [])
|
||||
|
||||
case :erlang.get(:elixir_code_diagnostics) do
|
||||
:undefined -> :ok
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
|
||||
end
|
||||
|
||||
{warnings, errors}
|
||||
diagnostics
|
||||
end
|
||||
|
||||
defp collect_results(0, warnings, errors) do
|
||||
{warnings, errors}
|
||||
defp collect_results(0, diagnostics) do
|
||||
diagnostics
|
||||
end
|
||||
|
||||
defp collect_results(count, warnings, errors) do
|
||||
defp collect_results(count, diagnostics) do
|
||||
receive do
|
||||
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
diagnostic = %{diagnostic | file: file && Path.absname(file)}
|
||||
collect_results(count, [diagnostic | diagnostics])
|
||||
|
||||
if Map.get(diagnostic, :severity, :warning) == :error do
|
||||
collect_results(count, warnings, [diagnostic | errors])
|
||||
else
|
||||
collect_results(count, [diagnostic | warnings], errors)
|
||||
end
|
||||
|
||||
{__MODULE__, _module, new_warnings, new_errors} ->
|
||||
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
|
||||
{__MODULE__, _module, new_diagnostics} ->
|
||||
collect_results(count - 1, new_diagnostics ++ diagnostics)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Test cache.
|
||||
"""
|
||||
def test_cache do
|
||||
{:ok, cache} = start_link()
|
||||
cache
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the export kind and deprecation reason for the given MFA from
|
||||
the cache. If the module does not exist return `:badmodule`,
|
||||
@@ -263,7 +264,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_tuple, cache, log?, profile) do
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
|
||||
module_tuple
|
||||
|
||||
@@ -277,23 +278,18 @@ defmodule Module.ParallelChecker do
|
||||
definitions
|
||||
)
|
||||
|
||||
type_warnings =
|
||||
profile(module, profile, fn ->
|
||||
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
|
||||
end)
|
||||
|
||||
{warnings, errors} =
|
||||
type_warnings
|
||||
diagnostics =
|
||||
module
|
||||
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|
||||
|> Kernel.++(behaviour_warnings)
|
||||
|> group_diagnostics()
|
||||
|> emit_diagnostics(file, log?)
|
||||
|> Enum.split_with(&(&1.severity == :warning))
|
||||
|> group_warnings()
|
||||
|> emit_warnings(file, log?)
|
||||
|
||||
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
|
||||
apply(verify_mod, verify_fun, [module])
|
||||
end)
|
||||
|
||||
{warnings, errors}
|
||||
diagnostics
|
||||
end
|
||||
|
||||
defp module_map_to_module_tuple(module_map) do
|
||||
@@ -344,16 +340,16 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Warning helpers
|
||||
|
||||
defp group_diagnostics(triplets) do
|
||||
defp group_warnings(warnings) do
|
||||
{ungrouped, grouped} =
|
||||
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(term)
|
||||
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(warning)
|
||||
|
||||
if Map.get(diagnostic, :group, false) do
|
||||
locations = MapSet.new([location])
|
||||
|
||||
grouped =
|
||||
Map.update(grouped, term, {locations, diagnostic}, fn
|
||||
Map.update(grouped, warning, {locations, diagnostic}, fn
|
||||
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
|
||||
end)
|
||||
|
||||
@@ -371,7 +367,7 @@ defmodule Module.ParallelChecker do
|
||||
Enum.sort(ungrouped ++ grouped)
|
||||
end
|
||||
|
||||
defp emit_diagnostics(warnings, file, log?) do
|
||||
defp emit_warnings(warnings, file, log?) do
|
||||
Enum.flat_map(warnings, fn {locations, diagnostic} ->
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
|
||||
log? and print_diagnostics(diagnostics)
|
||||
@@ -533,12 +529,8 @@ defmodule Module.ParallelChecker do
|
||||
:gen_server.call(server, {:unlock, module, mode}, :infinity)
|
||||
end
|
||||
|
||||
defp register_cache_and_checker(server, mode, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref})
|
||||
end
|
||||
|
||||
defp register_cache(server, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register_cache, module, pid, ref})
|
||||
defp register(server, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register, module, pid, ref})
|
||||
end
|
||||
|
||||
## Server callbacks
|
||||
@@ -581,13 +573,11 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
state = %{
|
||||
waiting: %{},
|
||||
caches: [],
|
||||
checkers: [],
|
||||
modules: [],
|
||||
spawned: %{},
|
||||
schedulers: schedulers,
|
||||
threshold: threshold,
|
||||
callback: callback,
|
||||
profile: Keyword.get(options, :profile),
|
||||
protocols: [],
|
||||
table: table
|
||||
}
|
||||
@@ -595,9 +585,20 @@ defmodule Module.ParallelChecker do
|
||||
:gen_server.enter_loop(__MODULE__, [], state)
|
||||
end
|
||||
|
||||
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do
|
||||
def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
|
||||
:ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
|
||||
{:reply, length(caches), run_caches(%{state | protocols: []})}
|
||||
|
||||
for {_module, pid, ref} <- modules do
|
||||
send(pid, {ref, :cache})
|
||||
end
|
||||
|
||||
for {_module, _pid, ref} <- modules do
|
||||
receive do
|
||||
{^ref, :cached} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
{:reply, length(modules), run_checkers(%{state | protocols: []})}
|
||||
end
|
||||
|
||||
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
|
||||
@@ -627,21 +628,10 @@ defmodule Module.ParallelChecker do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
|
||||
{_nil, spawned} = Map.pop(state.spawned, ref)
|
||||
|
||||
state =
|
||||
if pid do
|
||||
%{state | spawned: spawned, checkers: [{module, pid, ref} | state.checkers]}
|
||||
else
|
||||
%{state | spawned: spawned}
|
||||
end
|
||||
|
||||
{:noreply, run_caches(state)}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :checked, ref}, state) do
|
||||
{timer, spawned} = Map.pop(state.spawned, ref)
|
||||
def handle_info({__MODULE__, :done, module}, state) do
|
||||
# Unfortunately we cannot assume uniqueness because the same module
|
||||
# may be defined by mistake several times
|
||||
{timer, spawned} = Map.pop(state.spawned, module)
|
||||
timer && Process.cancel_timer(timer)
|
||||
{:noreply, run_checkers(%{state | spawned: spawned})}
|
||||
end
|
||||
@@ -650,41 +640,11 @@ defmodule Module.ParallelChecker do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
|
||||
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
|
||||
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
|
||||
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
|
||||
end
|
||||
|
||||
def handle_cast(
|
||||
{:register_cache_and_checker, mode, module, pid, ref},
|
||||
%{caches: caches, checkers: checkers} = state
|
||||
) do
|
||||
{:noreply,
|
||||
%{state | caches: [{module, mode} | caches], checkers: [{module, pid, ref} | checkers]}}
|
||||
end
|
||||
|
||||
defp run_caches(%{caches: [], spawned: spawned} = state) do
|
||||
if spawned == %{}, do: run_checkers(state), else: state
|
||||
end
|
||||
|
||||
defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
|
||||
when map_size(spawned) >= schedulers do
|
||||
state
|
||||
end
|
||||
|
||||
defp run_caches(%{caches: [cache | caches]} = state) do
|
||||
case cache do
|
||||
{_module, pid, ref} ->
|
||||
send(pid, {ref, :cache})
|
||||
spawned = Map.put(state.spawned, ref, nil)
|
||||
run_caches(%{state | caches: caches, spawned: spawned})
|
||||
|
||||
{module, mode} ->
|
||||
:ets.insert(state.table, {module, mode})
|
||||
run_caches(%{state | caches: caches})
|
||||
end
|
||||
end
|
||||
|
||||
defp run_checkers(%{checkers: []} = state) do
|
||||
defp run_checkers(%{modules: []} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
@@ -693,25 +653,10 @@ defmodule Module.ParallelChecker do
|
||||
state
|
||||
end
|
||||
|
||||
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
|
||||
send(pid, {ref, :check, state.profile})
|
||||
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
|
||||
spawned = Map.put(state.spawned, ref, timer)
|
||||
run_checkers(%{state | checkers: checkers, spawned: spawned})
|
||||
end
|
||||
|
||||
defp profile(module, :time, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
|
||||
)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp profile(_module, _profile, fun) do
|
||||
fun.()
|
||||
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
|
||||
send(pid, {ref, :check})
|
||||
timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
|
||||
spawned = Map.put(state.spawned, module, timer)
|
||||
run_checkers(%{state | modules: modules, spawned: spawned})
|
||||
end
|
||||
end
|
||||
|
||||
+85
-297
@@ -4,7 +4,7 @@
|
||||
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
|
||||
alias Module.Types.{Descr, Expr, Pattern, Helpers}
|
||||
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
@@ -24,17 +24,20 @@ defmodule Module.Types do
|
||||
#
|
||||
# * :infer - Same as :dynamic but skips remote calls.
|
||||
#
|
||||
# * :traversal - Focused mostly on traversing AST, skips most type system
|
||||
# operations. Used by macros and when skipping inference.
|
||||
#
|
||||
# The mode may also control exhaustiveness checks in the future (to be decided).
|
||||
# We may also want for applications with subtyping in dynamic mode to always
|
||||
# intersect with dynamic, but this mode may be too lax (to be decided based on
|
||||
# feedback).
|
||||
@modes [:static, :dynamic, :infer]
|
||||
@modes [:static, :dynamic, :infer, :traversal]
|
||||
|
||||
# These functions are not inferred because they are added/managed by the compiler
|
||||
@no_infer [behaviour_info: 1]
|
||||
|
||||
@doc false
|
||||
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do
|
||||
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
|
||||
# We don't care about inferring signatures for protocols,
|
||||
# those will be replaced anyway. There is also nothing to
|
||||
# infer if there is no cache system, we only do traversals.
|
||||
@@ -72,13 +75,12 @@ defmodule Module.Types do
|
||||
|
||||
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
|
||||
|
||||
# In case there are loops, the other we traverse matters,
|
||||
# so we sort the definitions for determinism
|
||||
{types, private, %{local_sigs: reachable_sigs} = context} =
|
||||
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
|
||||
reduce: {[], [], context()} do
|
||||
{types, private, context} when kind in [:def, :defmacro] ->
|
||||
# Optimized version of finder, since we already have the definition
|
||||
{types, %{local_sigs: reachable_sigs} = context} =
|
||||
for {fun_arity, kind, meta, _clauses} = def <- defs,
|
||||
kind in [:def, :defmacro],
|
||||
reduce: {[], context()} do
|
||||
{types, context} ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
finder = fn _ ->
|
||||
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
|
||||
end
|
||||
@@ -86,13 +88,10 @@ defmodule Module.Types do
|
||||
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
|
||||
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
|
||||
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
|
||||
{[{fun_arity, inferred} | types], context}
|
||||
else
|
||||
{types, private, context}
|
||||
{types, context}
|
||||
end
|
||||
|
||||
{types, private, context} ->
|
||||
{types, [def | private], context}
|
||||
end
|
||||
|
||||
# Now traverse all used privates to find any other private that have been used by them.
|
||||
@@ -106,8 +105,8 @@ defmodule Module.Types do
|
||||
|
||||
{unreachable, _context} =
|
||||
Enum.reduce(private, {[], context}, fn
|
||||
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
|
||||
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
|
||||
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
|
||||
warn_unused_def(info, used_sigs, env)
|
||||
|
||||
# Find anything undefined within unused functions
|
||||
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
|
||||
@@ -126,7 +125,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp infer_mode(kind, infer_signatures?) do
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
|
||||
end
|
||||
|
||||
defp protocol?(attrs) do
|
||||
@@ -136,8 +135,8 @@ defmodule Module.Types do
|
||||
defp impl_for(attrs) do
|
||||
case List.keyfind(attrs, :__impl__, 0) do
|
||||
{:__impl__, [protocol: protocol, for: for]} ->
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
|
||||
{for, protocol.__protocol__(:functions)}
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
|
||||
{for, protocol.behaviour_info(:callbacks)}
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -155,7 +154,7 @@ defmodule Module.Types do
|
||||
| List.duplicate(Descr.dynamic(), arity - 1)
|
||||
]
|
||||
|
||||
{_fun_arity, kind, meta, clauses} = def
|
||||
{fun_arity, kind, meta, clauses} = def
|
||||
|
||||
clauses =
|
||||
for {meta, args, guards, body} <- clauses do
|
||||
@@ -174,30 +173,29 @@ defmodule Module.Types do
|
||||
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
|
||||
end
|
||||
|
||||
defp warn_unused_def(fun_arity, kind, meta, used, env) do
|
||||
default = Keyword.get(meta, :defaults, 0)
|
||||
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
|
||||
:ok
|
||||
end
|
||||
|
||||
cond do
|
||||
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true ->
|
||||
:ok
|
||||
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
|
||||
default == 0 ->
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
:ok
|
||||
end
|
||||
|
||||
default > 0 ->
|
||||
{name, arity} = fun_arity
|
||||
min = arity - default
|
||||
max = arity
|
||||
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
case min_reachable_default(max, min, :none, name, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
|
||||
end
|
||||
case min_reachable_default(max, min, :none, name, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
|
||||
end
|
||||
|
||||
:ok
|
||||
@@ -251,27 +249,17 @@ defmodule Module.Types do
|
||||
context ->
|
||||
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
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)
|
||||
clauses_indexes =
|
||||
for type_index <- pending,
|
||||
not skip_unused_clause?(info, type_index),
|
||||
{clause_index, ^type_index} <- mapping,
|
||||
do: clause_index
|
||||
|
||||
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
|
||||
|
||||
Enum.reduce(unused_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
else
|
||||
context
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -299,11 +287,11 @@ defmodule Module.Types do
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
|
||||
{mode, {fun_arity, kind, meta, clauses}, expected} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
|
||||
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -316,124 +304,36 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
|
||||
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = fresh_stack(stack, mode, fun_arity)
|
||||
base_info = {:def, kind, fun, expected}
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
|
||||
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
|
||||
stack = with_file_meta(stack, meta)
|
||||
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} ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
{_, _, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, [], [], context}, fn
|
||||
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
|
||||
context = fresh_context(context)
|
||||
|
||||
try do
|
||||
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
|
||||
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
|
||||
{trees, context} =
|
||||
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context)
|
||||
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, Descr.term(), body, stack, head_context)
|
||||
Expr.of_expr(body, Descr.term(), body, stack, context)
|
||||
|
||||
args_types = Pattern.of_domain(trees, stack, context)
|
||||
args_types =
|
||||
if stack.mode == :traversal do
|
||||
expected
|
||||
else
|
||||
Pattern.of_domain(trees, expected, context)
|
||||
end
|
||||
|
||||
{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
|
||||
mapping = [{index, total} | mapping]
|
||||
{index + 1, total + 1, previous, domain, mapping, inferred, context}
|
||||
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
|
||||
else
|
||||
mapping = [{index, type_index} | mapping]
|
||||
{index + 1, total, previous, domain, mapping, inferred, context}
|
||||
{index + 1, total, [{index, type_index} | mapping], inferred, context}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@@ -443,69 +343,19 @@ defmodule Module.Types do
|
||||
|
||||
domain =
|
||||
case clauses_types do
|
||||
[_] -> nil
|
||||
_ -> domain
|
||||
[_] ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
clauses_types
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
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),
|
||||
@@ -517,58 +367,6 @@ defmodule Module.Types do
|
||||
defp add_inferred([], args, return, -1, acc),
|
||||
do: {-1, [{args, return} | Enum.reverse(acc)]}
|
||||
|
||||
# Compact clauses that have the same return and differ in exactly one
|
||||
# argument by unioning that argument. For example:
|
||||
#
|
||||
# (integer(), atom() -> boolean()) and (float(), atom() -> boolean())
|
||||
#
|
||||
# becomes:
|
||||
#
|
||||
# (number(), atom() -> boolean())
|
||||
#
|
||||
# Arity-zero clauses have no argument position to widen.
|
||||
defp group_clauses_by_return({:infer, domain, [{[_ | _], _} | _] = clauses}) do
|
||||
clauses =
|
||||
Enum.reduce(clauses, [], fn {args, return}, acc ->
|
||||
group_clause_by_return(acc, args, return)
|
||||
end)
|
||||
|
||||
{:infer, domain, clauses}
|
||||
end
|
||||
|
||||
defp group_clauses_by_return(info), do: info
|
||||
|
||||
defp group_clause_by_return([{existing_args, return} | tail], args, return) do
|
||||
case union_args(existing_args, args, [], false) do
|
||||
nil ->
|
||||
[{existing_args, return} | group_clause_by_return(tail, args, return)]
|
||||
|
||||
new_args ->
|
||||
[{new_args, return} | tail]
|
||||
end
|
||||
end
|
||||
|
||||
defp group_clause_by_return([head | tail], args, return) do
|
||||
[head | group_clause_by_return(tail, args, return)]
|
||||
end
|
||||
|
||||
defp group_clause_by_return([], args, return), do: [{args, return}]
|
||||
|
||||
defp union_args([arg | existing], [arg | args], acc, changed?) do
|
||||
union_args(existing, args, [arg | acc], changed?)
|
||||
end
|
||||
|
||||
# Allow exactly one differing argument. That one position is widened
|
||||
# with union/2. A second difference means the clauses must stay separate.
|
||||
defp union_args([existing_arg | existing], [arg | args], acc, false) do
|
||||
union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
|
||||
end
|
||||
|
||||
defp union_args([_ | _], [_ | _], _acc, true), do: nil
|
||||
|
||||
# In theory fully equal args are merged on add_inferred
|
||||
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
|
||||
|
||||
defp with_file_meta(stack, meta) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
|
||||
@@ -631,8 +429,9 @@ defmodule Module.Types do
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :use)
|
||||
reverse_arrow: nil
|
||||
# Control if variable refinement is enabled.
|
||||
# It is disabled only on dynamic dispatches.
|
||||
refine_vars: false
|
||||
}
|
||||
end
|
||||
|
||||
@@ -643,45 +442,34 @@ defmodule Module.Types do
|
||||
warnings: [],
|
||||
# All vars and their types
|
||||
vars: %{},
|
||||
# Stores special metadata used by list heads and domain keys in patterns
|
||||
subpatterns: %{},
|
||||
# Variables that are specific to the current environment/conditional
|
||||
conditional_vars: nil,
|
||||
# Track metadata specific to patterns and guards
|
||||
# Variables and arguments from patterns
|
||||
pattern_info: nil,
|
||||
# If type checking has found an error/failure
|
||||
failed: false,
|
||||
# Local signatures used by local handler
|
||||
local_sigs: %{},
|
||||
# Track which clauses have been used across private local calls
|
||||
local_used: %{},
|
||||
# Cached reverse arrows
|
||||
reverse_arrows: %{}
|
||||
local_used: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp fresh_stack(stack, mode, function) when mode in @modes do
|
||||
%{stack | mode: mode, function: function, reverse_arrow: nil}
|
||||
%{stack | mode: mode, function: function}
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
|
||||
%{context | vars: %{}, failed: false}
|
||||
end
|
||||
|
||||
defp restore_context(later_context, %{
|
||||
vars: vars,
|
||||
failed: failed,
|
||||
reverse_arrows: reverse_arrows
|
||||
}) do
|
||||
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
|
||||
defp restore_context(later_context, %{vars: vars, failed: failed}) do
|
||||
%{later_context | vars: vars, failed: failed}
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
|
||||
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
|
||||
%{
|
||||
message:
|
||||
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
|
||||
message: "this clause of #{kind} #{fun}/#{arity} is never used"
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
+225
-1368
File diff suppressed because it is too large
Load Diff
+1853
-3691
File diff suppressed because it is too large
Load Diff
+382
-682
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
|
||||
@doc """
|
||||
Returns true if the mode cares about warnings.
|
||||
"""
|
||||
defguard is_warning(stack) when stack.mode != :infer
|
||||
defguard is_warning(stack) when stack.mode not in [:traversal, :infer]
|
||||
|
||||
@doc """
|
||||
Guard function to check if an AST node is a variable.
|
||||
@@ -143,13 +143,12 @@ defmodule Module.Types.Helpers do
|
||||
"""
|
||||
def collect_traces(expr, %{vars: vars}) do
|
||||
{_, versions} =
|
||||
Macro.prewalk(expr, %{}, fn node, versions ->
|
||||
with {var_name, meta, var_context} when is_atom(var_name) and is_atom(var_context) <- node,
|
||||
false <- String.starts_with?(Atom.to_string(var_name), "_") do
|
||||
Macro.prewalk(expr, %{}, fn
|
||||
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
|
||||
version = meta[:version]
|
||||
|
||||
case vars do
|
||||
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
|
||||
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
|
||||
{:ok,
|
||||
Map.put(versions, version, %{
|
||||
type: :variable,
|
||||
@@ -161,9 +160,9 @@ defmodule Module.Types.Helpers do
|
||||
_ ->
|
||||
{:ok, versions}
|
||||
end
|
||||
else
|
||||
_ -> {node, versions}
|
||||
end
|
||||
|
||||
node, versions ->
|
||||
{node, versions}
|
||||
end)
|
||||
|
||||
versions
|
||||
@@ -262,8 +261,7 @@ defmodule Module.Types.Helpers do
|
||||
Converts the given expression to a string,
|
||||
translating inlined Erlang calls back to Elixir.
|
||||
|
||||
We also undo some macro expressions done by the Kernel module
|
||||
and collapse complex expressions.
|
||||
We also undo some macro expressions done by the Kernel module.
|
||||
|
||||
## Options
|
||||
|
||||
@@ -329,13 +327,6 @@ defmodule Module.Types.Helpers do
|
||||
end
|
||||
end
|
||||
|
||||
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
|
||||
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
|
||||
call
|
||||
else
|
||||
{:in, meta, [expr, args]}
|
||||
end
|
||||
|
||||
{{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} ->
|
||||
{:to_string, meta, [arg]}
|
||||
|
||||
@@ -361,134 +352,42 @@ defmodule Module.Types.Helpers do
|
||||
|
||||
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
|
||||
|
||||
{:case, meta, [expr, [do: clauses]]} ->
|
||||
case meta[:type_check] do
|
||||
{:case, op} ->
|
||||
case clauses do
|
||||
[
|
||||
{:->, _,
|
||||
{:case, meta, [expr, [do: clauses]]} = case ->
|
||||
if meta[:type_check] == :expr do
|
||||
case clauses do
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
true
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], false]}
|
||||
]
|
||||
when op == :! ->
|
||||
{:!, meta, [expr]}
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
else_block
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], do_block]}
|
||||
] ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
right_side
|
||||
]},
|
||||
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
|
||||
]
|
||||
when op == :|| ->
|
||||
{:||, meta, [expr, right_side]}
|
||||
[
|
||||
{:->, _, [[false], else_block]},
|
||||
{:->, _, [[true], do_block]}
|
||||
] ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
{var, _, Kernel}
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], right_side]}
|
||||
]
|
||||
when op == :&& ->
|
||||
{:&&, meta, [expr, right_side]}
|
||||
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
else_block
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], do_block]}
|
||||
]
|
||||
when op == :if ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
|
||||
[
|
||||
{:->, _, [[false], else_block]},
|
||||
{:->, _, [[true], do_block]}
|
||||
]
|
||||
when op == :if ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
|
||||
[
|
||||
{:->, _, [[false], false]},
|
||||
{:->, _, [[true], right]}
|
||||
| _
|
||||
]
|
||||
when op == :and ->
|
||||
{:and, meta, [expr, right]}
|
||||
|
||||
[
|
||||
{:->, _, [[false], right]},
|
||||
{:->, _, [[true], true]}
|
||||
| _
|
||||
]
|
||||
when op == :or ->
|
||||
{:or, meta, [expr, right]}
|
||||
|
||||
_ ->
|
||||
{:case, meta, [expr, [do: {:..., [], []}]]}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:case, meta, [expr, [do: {:..., [], []}]]}
|
||||
_ ->
|
||||
case
|
||||
end
|
||||
else
|
||||
case
|
||||
end
|
||||
|
||||
{:try, meta, [[do: _] ++ _]} ->
|
||||
{:try, meta, [[do: {:..., [], []}]]}
|
||||
|
||||
{:cond, meta, [[do: _]]} ->
|
||||
{:cond, meta, [[do: {:..., [], []}]]}
|
||||
|
||||
{:receive, meta, [[do: _] ++ _]} ->
|
||||
{:receive, meta, [[do: {:..., [], []}]]}
|
||||
|
||||
{var, meta, context} = expr when is_atom(var) and is_atom(context) ->
|
||||
if is_integer(meta[:capture]) do
|
||||
{:&, meta, [meta[:capture]]}
|
||||
|
||||
+211
-450
@@ -1,6 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
@@ -12,10 +11,10 @@ defmodule Module.Types.Of do
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float union(integer(), float())
|
||||
@integer_or_binary union(integer(), binary())
|
||||
@integer integer()
|
||||
@float float()
|
||||
@binary binary()
|
||||
@bitstring bitstring()
|
||||
|
||||
## Variables
|
||||
|
||||
@@ -30,53 +29,19 @@ defmodule Module.Types.Of do
|
||||
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
|
||||
This purposedly deletes all traces of the variable,
|
||||
as it is often invoked when the cause for error is elsewhere.
|
||||
"""
|
||||
def error_var({_, meta, _}, context) do
|
||||
error_var(Keyword.fetch!(meta, :version), context)
|
||||
end
|
||||
|
||||
def error_var(version, context) do
|
||||
update_in(context.vars[version], fn
|
||||
%{errored: true} = data -> data
|
||||
data -> Map.put(%{data | type: error_type(), off_traces: []}, :errored, true)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares a variable.
|
||||
"""
|
||||
def declare_var(var, type \\ term(), context) do
|
||||
def error_var(var, context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
case context.vars do
|
||||
%{^version => _} ->
|
||||
context
|
||||
data = %{
|
||||
type: error_type(),
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: []
|
||||
}
|
||||
|
||||
vars ->
|
||||
data = %{
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: [],
|
||||
paths: [],
|
||||
deps: %{}
|
||||
}
|
||||
|
||||
%{context | vars: Map.put(vars, version, data)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Tracks metadata about variables dependencies and paths.
|
||||
"""
|
||||
def track_var(version, new_deps, new_paths, context) do
|
||||
update_in(context.vars[version], fn %{paths: paths, deps: deps} = data ->
|
||||
%{data | paths: new_paths ++ paths, deps: Enum.reduce(new_deps, deps, &Map.put(&2, &1, []))}
|
||||
end)
|
||||
put_in(context.vars[version], data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -86,70 +51,26 @@ defmodule Module.Types.Of do
|
||||
or if we are doing a guard analysis or occurrence typing.
|
||||
Returns `true` if there was a refinement, `false` otherwise.
|
||||
"""
|
||||
@skip_refinement_for [term(), dynamic()]
|
||||
def refine_body_var(var_or_version, type, expr, stack, context)
|
||||
|
||||
def refine_body_var({_, meta, _}, type, expr, stack, context) do
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_body_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
|
||||
|
||||
context =
|
||||
case context.conditional_vars do
|
||||
%{} = conditional_vars ->
|
||||
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] do
|
||||
case compatible_intersection(old_type, type) do
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
|
||||
nil ->
|
||||
context
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
|
||||
case context do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
%{pattern_info: %{guard_context: guard_context}} ->
|
||||
new_type = intersection(old_type, type)
|
||||
|
||||
case empty?(new_type) do
|
||||
true when guard_context == :orelse ->
|
||||
data = %{
|
||||
data
|
||||
| type: none(),
|
||||
off_traces: new_trace(expr, none(), stack, off_traces)
|
||||
}
|
||||
|
||||
{none(), %{context | vars: %{vars | version => data}}}
|
||||
|
||||
false when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
|
||||
_ ->
|
||||
case gradual?(old_type) and compatible_intersection(old_type, type) do
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
else
|
||||
{old_type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,16 +81,11 @@ defmodule Module.Types.Of do
|
||||
because we want to refine types. Otherwise we should
|
||||
use compatibility.
|
||||
"""
|
||||
def refine_head_var({_, meta, _}, type, expr, stack, context) do
|
||||
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
def refine_head_var(var, type, expr, stack, context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
def refine_head_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
case context.vars do
|
||||
%{^version => %{errored: true}} ->
|
||||
{:ok, error_type(), context}
|
||||
|
||||
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
|
||||
new_type = intersection(type, old_type)
|
||||
|
||||
@@ -179,14 +95,26 @@ defmodule Module.Types.Of do
|
||||
off_traces: new_trace(expr, type, stack, off_traces)
|
||||
}
|
||||
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
|
||||
# We need to return error otherwise it leads to cascading errors
|
||||
if empty?(new_type) do
|
||||
data = Map.put(%{data | type: error_type()}, :errored, true)
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
{:error, old_type, context}
|
||||
{:error, error_type(),
|
||||
error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
|
||||
else
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
{:ok, new_type, context}
|
||||
end
|
||||
|
||||
%{} = vars ->
|
||||
data = %{
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: new_trace(expr, type, stack, [])
|
||||
}
|
||||
|
||||
context = %{context | vars: Map.put(vars, version, data)}
|
||||
{:ok, type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -196,71 +124,11 @@ defmodule Module.Types.Of do
|
||||
defp new_trace(expr, type, stack, traces),
|
||||
do: [{expr, stack.file, type} | traces]
|
||||
|
||||
@doc """
|
||||
Preserves `context` in first argument while
|
||||
resetting it to the vars in the second argument.
|
||||
"""
|
||||
def reset_vars(context, %{
|
||||
subpatterns: subpatterns,
|
||||
vars: vars,
|
||||
conditional_vars: conditional_vars
|
||||
}),
|
||||
do: %{context | subpatterns: subpatterns, vars: vars, conditional_vars: conditional_vars}
|
||||
|
||||
@doc """
|
||||
Returns true if all entries have the same conditional vars.
|
||||
"""
|
||||
def all_same_conditional_vars?([{_, cond} | tail]) do
|
||||
Enum.all?(tail, fn {_, tail_cond} -> cond == tail_cond end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes the args with acc using conditional variables.
|
||||
"""
|
||||
def with_conditional_vars(args, acc, expr, stack, context, fun) do
|
||||
%{vars: vars, conditional_vars: conditional_vars} = context
|
||||
|
||||
{vars_conds, {acc, context}} =
|
||||
Enum.map_reduce(args, {acc, context}, fn arg, {acc, context} ->
|
||||
{acc, context} = fun.(arg, acc, %{context | vars: vars, conditional_vars: %{}})
|
||||
%{vars: vars, conditional_vars: cond_vars} = context
|
||||
{{vars, cond_vars}, {acc, context}}
|
||||
end)
|
||||
|
||||
context = %{context | vars: vars, conditional_vars: conditional_vars}
|
||||
{acc, reduce_conditional_vars(vars_conds, expr, stack, context)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reduces conditional variables collected separately.
|
||||
"""
|
||||
def reduce_conditional_vars([{vars, cond} | vars_conds], expr, stack, context) do
|
||||
%{vars: pre_vars} = context
|
||||
|
||||
Enum.reduce(Map.keys(cond), context, fn version, context ->
|
||||
if is_map_key(pre_vars, version) and
|
||||
Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
|
||||
%{^version => %{type: type}} = vars
|
||||
|
||||
type =
|
||||
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
|
||||
%{^version => %{type: type}} = vars
|
||||
union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = refine_body_var(version, type, expr, stack, context)
|
||||
context
|
||||
else
|
||||
context
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
## Implementations
|
||||
|
||||
impls = [
|
||||
{Atom, atom()},
|
||||
{BitString, bitstring()},
|
||||
{BitString, binary()},
|
||||
{Float, float()},
|
||||
{Function, fun()},
|
||||
{Integer, integer()},
|
||||
@@ -304,7 +172,7 @@ defmodule Module.Types.Of do
|
||||
Handles fetching a map key.
|
||||
"""
|
||||
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
|
||||
case map_fetch_key(type, field) do
|
||||
case map_fetch(type, field) do
|
||||
{_optional?, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
@@ -319,134 +187,107 @@ defmodule Module.Types.Of do
|
||||
def closed_map(pairs, expected, stack, context, of_fun) do
|
||||
{pairs_types, context} = pairs(pairs, expected, stack, context, of_fun)
|
||||
|
||||
{dynamic?, domain, single, multiple} =
|
||||
Enum.reduce(pairs_types, {false, [], [], []}, fn
|
||||
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
|
||||
dynamic? = dynamic? or dynamic_pair?
|
||||
|
||||
case pos_neg_domain do
|
||||
# If atom is included in domain keys, it unions all previous
|
||||
# single and multiple, except the ones negated:
|
||||
#
|
||||
# %{foo: :bar, term() => :baz}
|
||||
# #=> %{foo: :bar or :baz, term() => :baz}
|
||||
#
|
||||
# %{foo: :bar, not :foo => :baz}
|
||||
# #=> %{foo: :bar, term() => :baz}
|
||||
#
|
||||
# In case the negated term does not appear, we set it to none():
|
||||
#
|
||||
# %{foo: :bar, term() => :baz}
|
||||
# #=> %{term() => :baz, foo: :bar or :baz}
|
||||
#
|
||||
# %{not :foo => :baz}
|
||||
# #=> %{term() => :baz, foo: none()}
|
||||
#
|
||||
# In case we are dealing with multiple keys, we always merge the
|
||||
# domain. A more precise approach would be to postpone doing so
|
||||
# until the cartesian map is distributed but those should be very
|
||||
# uncommon.
|
||||
{[], negs, domain_keys} ->
|
||||
if :atom in domain_keys do
|
||||
{single, multiple} = union_negated(negs, value_type, single, multiple)
|
||||
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
|
||||
else
|
||||
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
|
||||
end
|
||||
|
||||
{pos, [], domain_keys} ->
|
||||
domain =
|
||||
case domain_keys do
|
||||
[] -> domain
|
||||
_ -> [{domain_keys, value_type} | domain]
|
||||
end
|
||||
|
||||
case pos do
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, domain, [{key, value_type} | single], multiple}
|
||||
|
||||
_ ->
|
||||
{dynamic?, domain, single, [{pos, value_type} | multiple]}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
non_multiple = Enum.reverse(single, domain)
|
||||
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
closed_map(non_multiple)
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
closed_map(non_multiple ++ [{key, type} | t])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
end
|
||||
|
||||
{if(dynamic?, do: dynamic(map), else: map), context}
|
||||
end
|
||||
|
||||
defp union_negated([], new_type, single, multiple) do
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
|
||||
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
|
||||
{single, multiple}
|
||||
end
|
||||
|
||||
defp union_negated(negated, new_type, single, multiple) do
|
||||
{single, matched} =
|
||||
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
|
||||
if key in negated do
|
||||
{{key, old_type}, [key | matched]}
|
||||
else
|
||||
{{key, union(old_type, new_type)}, matched}
|
||||
end
|
||||
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
|
||||
# TODO: Use the fallback type to actually indicate if open or closed.
|
||||
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
|
||||
{map, context}
|
||||
end
|
||||
|
||||
defp pairs(pairs, expected, stack, context, of_fun) do
|
||||
@doc """
|
||||
Computes the types of key-value pairs.
|
||||
"""
|
||||
def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
|
||||
{_key_type, context} = of_fun.(key, term(), stack, context)
|
||||
{value_type, context} = of_fun.(value, term(), stack, context)
|
||||
{{true, :none, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
def pairs(pairs, expected, stack, context, of_fun) do
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
|
||||
|
||||
expected_value_type =
|
||||
with {[key], [], []} <- pos_neg_domain,
|
||||
{_, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{value_type, context} = of_fun.(value, expected_value_type, stack, context)
|
||||
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context}
|
||||
{{dynamic_key? or gradual?(value_type), keys, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{{[key], [], []}, false, context}
|
||||
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{false, [key], context}
|
||||
end
|
||||
|
||||
defp map_key_type(key, stack, context, of_fun) do
|
||||
defp finite_key_type(key, stack, context, of_fun) do
|
||||
{key_type, context} = of_fun.(key, term(), stack, context)
|
||||
domain_keys = to_domain_keys(key_type)
|
||||
|
||||
pos_neg_domain =
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
|
||||
{:infinite, list} -> {[], list, domain_keys}
|
||||
:error -> {[], [], domain_keys}
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {gradual?(key_type), list, context}
|
||||
_ -> {gradual?(key_type), :none, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds permutation of maps according to the given pairs types.
|
||||
"""
|
||||
def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do
|
||||
dynamic()
|
||||
end
|
||||
|
||||
def permutate_map(pairs_types, _stack, of_map) do
|
||||
{dynamic?, fallback, single, multiple, assert} =
|
||||
Enum.reduce(pairs_types, {false, none(), [], [], []}, fn
|
||||
{dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} ->
|
||||
dynamic? = dynamic? or dynamic_pair?
|
||||
|
||||
case keys do
|
||||
:none ->
|
||||
fallback = union(fallback, value_type)
|
||||
|
||||
{fallback, assert} =
|
||||
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
|
||||
{union(fallback, type), [key | assert]}
|
||||
end)
|
||||
|
||||
{fallback, assert} =
|
||||
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
|
||||
{union(fallback, type), keys ++ assert}
|
||||
end)
|
||||
|
||||
{dynamic?, fallback, [], [], assert}
|
||||
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, fallback, [{key, value_type} | single], multiple, assert}
|
||||
|
||||
keys ->
|
||||
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert}
|
||||
end
|
||||
end)
|
||||
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
end
|
||||
|
||||
{pos_neg_domain, gradual?(key_type), context}
|
||||
if dynamic?, do: dynamic(map), else: map
|
||||
end
|
||||
|
||||
defp cartesian_map(lists) do
|
||||
@@ -461,59 +302,50 @@ defmodule Module.Types.Of do
|
||||
|
||||
@doc """
|
||||
Handles instantiation of a new struct.
|
||||
|
||||
This is expanded and validated by the compiler, so don't need to check the fields.
|
||||
"""
|
||||
# TODO: Type check the fields match the struct
|
||||
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
|
||||
def struct_instance(struct, args, expected, meta, %{mode: mode} = stack, context, of_fun)
|
||||
when is_atom(struct) do
|
||||
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
|
||||
{_info, context} = struct_info(struct, meta, stack, context)
|
||||
|
||||
if is_nil(info) do
|
||||
{dynamic(), context}
|
||||
else
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
with true <- mode != :traversal,
|
||||
{_, expected_value_type} <- map_fetch(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
end
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `__info__(:struct)` information about a struct.
|
||||
"""
|
||||
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
|
||||
def struct_info(struct, meta, stack, context) do
|
||||
case stack.no_warn_undefined do
|
||||
%Macro.Env{} = env ->
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
|
||||
{:ok, info} -> {info, context}
|
||||
{:error, _desc} -> {nil, context}
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Fetch the signature to validate for warnings.
|
||||
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
info =
|
||||
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
|
||||
struct.__info__(:struct)
|
||||
struct.__info__(:struct) ||
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
|
||||
if info do
|
||||
{_, context} =
|
||||
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
{info, context}
|
||||
else
|
||||
error = {:unknown_struct, kind, struct, must_exist?}
|
||||
{nil, error(error, meta, stack, context)}
|
||||
end
|
||||
{info, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -530,74 +362,45 @@ defmodule Module.Types.Of do
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns shared error for unknown struct field.
|
||||
"""
|
||||
def unknown_struct_field(struct, field, kind, meta, stack, context) do
|
||||
error = {:unknown_struct_field, kind, struct, field}
|
||||
error(error, meta, stack, context)
|
||||
end
|
||||
|
||||
## Bitstrings
|
||||
## Binary
|
||||
|
||||
@doc """
|
||||
Handles bitstrings.
|
||||
Handles binaries.
|
||||
|
||||
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
|
||||
based on the position of the expression within the binary.
|
||||
"""
|
||||
def bitstring([], _kind, _stack, context) do
|
||||
{binary(), context}
|
||||
def binary([], _kind, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
def bitstring([head], kind, stack, context) do
|
||||
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
|
||||
{alignment_to_type(alignment), context}
|
||||
def binary([head], kind, stack, context) do
|
||||
binary_segment(head, kind, [head], stack, context)
|
||||
end
|
||||
|
||||
def bitstring([head | tail], kind, stack, context) do
|
||||
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context)
|
||||
bitstring_tail(tail, alignment, kind, stack, context)
|
||||
def binary([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
end
|
||||
|
||||
defp bitstring_tail([last], alignment, kind, stack, context) do
|
||||
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context)
|
||||
{alignment_to_type(alignment(seg_alignment, alignment)), context}
|
||||
defp binary_many([last], kind, stack, context) do
|
||||
binary_segment(last, kind, [@prefix, last], stack, context)
|
||||
end
|
||||
|
||||
defp bitstring_tail([head | tail], alignment, kind, stack, context) do
|
||||
{seg_alignment, context} =
|
||||
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
|
||||
|
||||
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
|
||||
defp binary_many([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
end
|
||||
|
||||
defp alignment(left, right) when is_integer(left) and is_integer(right), do: left + right
|
||||
defp alignment(_left, _right), do: :unknown
|
||||
|
||||
defp alignment_to_type(:unknown), do: bitstring()
|
||||
defp alignment_to_type(integer) when rem(integer, 8) == 0, do: binary()
|
||||
defp alignment_to_type(_integer), do: bitstring_no_binary()
|
||||
|
||||
# If the segment is a literal, the compiler has already checked its validity,
|
||||
# so we just check the size.
|
||||
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context)
|
||||
# so we just skip it.
|
||||
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
|
||||
when is_binary(left) or is_number(left) do
|
||||
{_type, alignment_type} = specifier_type(kind, right)
|
||||
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
|
||||
|
||||
# We don't need to check for bitstrings because the left side
|
||||
# is either a binary (aligned), float (aligned), or integer
|
||||
# (which we check below).
|
||||
if alignment_type == :integer and alignment_value != :default do
|
||||
{alignment_value, context}
|
||||
else
|
||||
{0, context}
|
||||
end
|
||||
context
|
||||
end
|
||||
|
||||
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
{type, alignment_type} = specifier_type(kind, right)
|
||||
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
type = specifier_type(kind, right)
|
||||
expr = {:<<>>, meta, args}
|
||||
|
||||
{actual, context} =
|
||||
@@ -614,26 +417,10 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
if compatible?(actual, type) do
|
||||
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
|
||||
|
||||
case alignment_type do
|
||||
:aligned ->
|
||||
{0, context}
|
||||
|
||||
:integer when alignment_value == :default ->
|
||||
{0, context}
|
||||
|
||||
# There is no size, so the alignment depends on the type.
|
||||
# If the type is exclusively a binary, then it is aligned.
|
||||
:bitstring when alignment_value == :default ->
|
||||
if bitstring_no_binary_type?(actual), do: {:unknown, context}, else: {0, context}
|
||||
|
||||
_ ->
|
||||
{alignment_value, context}
|
||||
end
|
||||
specifier_size(kind, right, stack, context)
|
||||
else
|
||||
error = {:badbinary, kind, meta, expr, type, actual, context}
|
||||
{:unknown, error(error, meta, stack, context)}
|
||||
error(error, meta, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -649,48 +436,39 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
|
||||
defp specifier_type(:match, {:utf8, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned}
|
||||
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned}
|
||||
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer}
|
||||
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring}
|
||||
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring}
|
||||
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned}
|
||||
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned}
|
||||
defp specifier_type(_kind, _specifier), do: {@integer, :integer}
|
||||
defp specifier_type(:match, {:utf8, _, _}), do: @integer
|
||||
defp specifier_type(:match, {:utf16, _, _}), do: @integer
|
||||
defp specifier_type(:match, {:utf32, _, _}), do: @integer
|
||||
defp specifier_type(:match, {:float, _, _}), do: @float
|
||||
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
|
||||
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
|
||||
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
|
||||
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
|
||||
defp specifier_type(_kind, {:integer, _, _}), do: @integer
|
||||
defp specifier_type(_kind, {:bits, _, _}), do: @binary
|
||||
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
|
||||
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
|
||||
defp specifier_type(_kind, {:binary, _, _}), do: @binary
|
||||
defp specifier_type(_kind, _specifier), do: @integer
|
||||
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
|
||||
end
|
||||
|
||||
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context})
|
||||
when is_integer(arg) do
|
||||
size = if unit == :default, do: arg, else: arg * unit
|
||||
{size, context}
|
||||
end
|
||||
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
compatible_size(actual, expr, stack, context)
|
||||
end
|
||||
|
||||
defp specifier_size(_match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
compatible_size(actual, expr, stack, context)
|
||||
end
|
||||
|
||||
# We currently assume the unit always comes before size
|
||||
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do
|
||||
{unit, context}
|
||||
end
|
||||
|
||||
defp specifier_size(_kind, _specifier, _stack, align_context) do
|
||||
align_context
|
||||
defp specifier_size(_kind, _specifier, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
defp compatible_size(actual, expr, stack, context) do
|
||||
@@ -711,12 +489,9 @@ defmodule Module.Types.Of do
|
||||
"""
|
||||
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
|
||||
case atom_fetch(type) do
|
||||
{:finite, mods} ->
|
||||
{_, mods} ->
|
||||
{mods, context}
|
||||
|
||||
{:infinite, _} ->
|
||||
{[], context}
|
||||
|
||||
:error ->
|
||||
warning = {:badmodule, expr, type, fun, arity, hints, context}
|
||||
{[], error(warning, meta, stack, context)}
|
||||
@@ -729,6 +504,23 @@ defmodule Module.Types.Of do
|
||||
error(__MODULE__, warning, meta, stack, context)
|
||||
end
|
||||
|
||||
def format_diagnostic({:refine_head_var, old_type, new_type, var, context}) do
|
||||
traces = collect_traces(var, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
incompatible types assigned to #{format_var(var)}:
|
||||
|
||||
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do
|
||||
type = if kind == :match, do: "matching", else: "construction"
|
||||
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
|
||||
@@ -848,37 +640,6 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
|
||||
detail =
|
||||
case {Code.ensure_loaded?(module), must_exist?} do
|
||||
{true, false} ->
|
||||
"there is such module but it does not define a struct"
|
||||
|
||||
{false, false} ->
|
||||
"module #{inspect(module)} is not available or is yet to be defined"
|
||||
|
||||
{true, true} ->
|
||||
"the module may have been redefined as it no longer defines a struct"
|
||||
|
||||
{false, true} ->
|
||||
"the module was also only available but may have been removed during compilation"
|
||||
end
|
||||
|
||||
%{
|
||||
message: "struct #{inspect(module)} is undefined (#{detail})",
|
||||
group: true,
|
||||
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
|
||||
%{
|
||||
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
|
||||
group: true,
|
||||
severity: if(kind == :pattern, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
defp dot_var?(expr) do
|
||||
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
|
||||
end
|
||||
|
||||
+477
-1423
File diff suppressed because it is too large
Load Diff
@@ -1,210 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
defmodule Module.Types.Traverse do
|
||||
@moduledoc false
|
||||
|
||||
# Traverses expressions to find local calls when inference is disabled.
|
||||
|
||||
# Literals
|
||||
def of_expr(literal, _stack, context)
|
||||
when is_atom(literal) or is_integer(literal) or is_float(literal) or is_binary(literal) or
|
||||
is_pid(literal) or literal == [] do
|
||||
context
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_expr(list, stack, context) when is_list(list) do
|
||||
Enum.reduce(list, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_expr({left, right}, stack, context) do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# <<...>>
|
||||
def of_expr({:<<>>, _meta, args}, stack, context) do
|
||||
Enum.reduce(args, context, fn
|
||||
{:"::", _meta, [left, _right]}, context ->
|
||||
of_expr(left, stack, context)
|
||||
|
||||
expr, context ->
|
||||
of_expr(expr, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]}, stack, context) do
|
||||
context = of_expr(map, stack, context)
|
||||
context = of_expr(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
def of_expr({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) do
|
||||
context = of_expr(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
# Map update, tail operator
|
||||
def of_expr({:|, _meta, [left, right]}, stack, context) do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# Tuples, maps
|
||||
def of_expr({container, _meta, exprs}, stack, context) when container in [:{}, :%{}] do
|
||||
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# left = right, left <- right
|
||||
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:=, :<-] do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# Blocks
|
||||
def of_expr({:__block__, _, args}, stack, context) do
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# cond do ... end
|
||||
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
|
||||
Enum.reduce(clauses, context, fn {:->, _meta, [[head], body]}, context ->
|
||||
context = of_expr(head, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# All non-handled -> are patterns
|
||||
def of_expr({:->, _, [head, body]}, stack, context) do
|
||||
context = of_pattern(head, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end
|
||||
|
||||
# case expr do ... end
|
||||
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
|
||||
context = of_expr(case_expr, stack, context)
|
||||
of_expr(clauses, stack, context)
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses}, stack, context) do
|
||||
of_expr(clauses, stack, context)
|
||||
end
|
||||
|
||||
# try do ... end
|
||||
def of_expr({:try, _meta, [blocks]}, stack, context) do
|
||||
Enum.reduce(blocks, context, fn {_, clauses_or_body}, context ->
|
||||
of_expr(clauses_or_body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# receive do ... end
|
||||
def of_expr({:receive, _meta, [blocks]}, stack, context) do
|
||||
Enum.reduce(blocks, context, fn
|
||||
{:do, clauses_or_empty_body}, context ->
|
||||
of_expr(clauses_or_empty_body, stack, context)
|
||||
|
||||
{:after, [{:->, _meta, [[timeout], body]}]}, context ->
|
||||
context = of_expr(timeout, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# for, with
|
||||
def of_expr({op, _meta, [_ | _] = args}, stack, context) when op in [:for, :with] do
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# fun.(args)
|
||||
def of_expr({{:., _meta, [fun]}, _call_meta, args}, stack, context) do
|
||||
context = of_expr(fun, stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# remote.fun(args)
|
||||
def of_expr({{:., _, [remote, name]}, _meta, args}, stack, context)
|
||||
when is_atom(name) do
|
||||
context = of_expr(remote, stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# &Mod.fun/arity
|
||||
def of_expr({:&, _, [{:/, _, [{{:., _, [_remote, name]}, _, []}, arity]}]}, _stack, context)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
context
|
||||
end
|
||||
|
||||
# &fun/arity
|
||||
def of_expr({:&, meta, [{:/, _, [{name, _, _ctx}, arity]}]}, stack, context)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
local_fun(meta, name, arity, stack, context)
|
||||
end
|
||||
|
||||
# super(args)
|
||||
def of_expr({:super, meta, args}, stack, context) when is_list(args) do
|
||||
{_kind, name} = Keyword.fetch!(meta, :super)
|
||||
context = local_fun(meta, name, length(args), stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# local_fun(args)
|
||||
def of_expr({name, meta, args}, stack, context)
|
||||
when is_atom(name) and is_list(args) do
|
||||
context = local_fun(meta, name, length(args), stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr({name, _meta, ctx}, _stack, context)
|
||||
when is_atom(name) and is_atom(ctx) do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_pattern({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) when is_atom(module) do
|
||||
context = of_pattern(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern({left, _meta, right}, stack, context) do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_pattern(right, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern({left, right}, stack, context) do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_pattern(right, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern([_ | _] = list, stack, context) do
|
||||
Enum.reduce(list, context, &of_pattern(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp of_pattern(_, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_struct(module, pairs, kind, meta, stack, context) do
|
||||
{info, context} = Module.Types.Of.struct_info(module, kind, meta, stack, context)
|
||||
|
||||
if info do
|
||||
Enum.reduce(pairs, context, fn {key, _value}, context ->
|
||||
if Enum.any?(info, &(&1.field == key)) do
|
||||
context
|
||||
else
|
||||
Module.Types.Of.unknown_struct_field(module, key, kind, meta, stack, context)
|
||||
end
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp local_fun(meta, fun, arity, stack, context) do
|
||||
case stack.local_handler.(meta, {fun, arity}, stack, context) do
|
||||
false -> context
|
||||
{_kind, _info, context} -> context
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -952,14 +952,8 @@ defmodule OptionParser do
|
||||
|
||||
case types |> List.delete(:keep) |> List.first(:string) do
|
||||
:boolean ->
|
||||
case to_switch(name) do
|
||||
"--no-" <> _ = switch ->
|
||||
add_aliases(switch, name, reverse_aliases)
|
||||
|
||||
switch ->
|
||||
base = "#{switch}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
end
|
||||
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
|
||||
type ->
|
||||
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
|
||||
|
||||
@@ -170,8 +170,6 @@ defmodule PartitionSupervisor do
|
||||
| {:max_seconds, non_neg_integer()}
|
||||
| {:with_arguments, (args :: [term()], partition() -> updated_args :: [term()])}
|
||||
|
||||
defguardp is_name(name) when is_atom(name) or elem(name, 0) == :via
|
||||
|
||||
@doc false
|
||||
def child_spec(opts) when is_list(opts) do
|
||||
id =
|
||||
@@ -368,7 +366,7 @@ defmodule PartitionSupervisor do
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
|
||||
def resize!(name, partitions) when is_name(name) and is_integer(partitions) do
|
||||
def resize!(name, partitions) when is_integer(partitions) do
|
||||
supervisor =
|
||||
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
|
||||
|
||||
@@ -423,8 +421,8 @@ defmodule PartitionSupervisor do
|
||||
Returns the number of partitions for the partition supervisor.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec partitions(name()) :: non_neg_integer()
|
||||
def partitions(name) when is_name(name) do
|
||||
@spec partitions(name()) :: pos_integer()
|
||||
def partitions(name) do
|
||||
name |> table() |> partitions(name)
|
||||
end
|
||||
|
||||
@@ -452,7 +450,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
Returns a list with information about all children.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
@@ -472,7 +470,7 @@ defmodule PartitionSupervisor do
|
||||
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
|
||||
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
|
||||
]
|
||||
def which_children(name) when is_name(name) do
|
||||
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
|
||||
Supervisor.which_children(name)
|
||||
end
|
||||
|
||||
@@ -500,7 +498,7 @@ defmodule PartitionSupervisor do
|
||||
supervisors: non_neg_integer,
|
||||
workers: non_neg_integer
|
||||
}
|
||||
def count_children(supervisor) when is_name(supervisor) do
|
||||
def count_children(supervisor) when is_atom(supervisor) do
|
||||
Supervisor.count_children(supervisor)
|
||||
end
|
||||
|
||||
@@ -516,7 +514,7 @@ defmodule PartitionSupervisor do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec stop(name(), reason :: term, timeout) :: :ok
|
||||
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
|
||||
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do
|
||||
Supervisor.stop(supervisor, reason, timeout)
|
||||
end
|
||||
|
||||
@@ -548,7 +546,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unregister_name(_) do
|
||||
def unregister_name(_, _) do
|
||||
raise "{:via, PartitionSupervisor, _} cannot be given on unregistration"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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)
|
||||
_ -> IO.chardata_to_string(path)
|
||||
_ -> path
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+27
-43
@@ -202,23 +202,24 @@ defmodule Process do
|
||||
@doc """
|
||||
Sends an exit signal with the given `reason` to `pid`.
|
||||
|
||||
Exit behavior differs based on the value of `reason`:
|
||||
The following behavior applies if `reason` is any term except `:normal`
|
||||
or `:kill`:
|
||||
|
||||
- If `:normal`, `pid` will not exit unless it is the calling process, in
|
||||
which case it will exit with the reason `:normal`. If it is trapping exits,
|
||||
the exit signal is transformed into a message `{:EXIT, from, :normal}` and
|
||||
delivered to its message queue.
|
||||
1. If `pid` is not trapping exits, `pid` will exit with the given
|
||||
`reason`.
|
||||
|
||||
- If `:kill`, which occurs when `Process.exit(pid, :kill)` is called, an
|
||||
untrappable exit signal is sent to `pid` which will unconditionally exit
|
||||
with reason `:killed`.
|
||||
2. If `pid` is trapping exits, the exit signal is transformed into a
|
||||
message `{:EXIT, from, reason}` and delivered to the message queue
|
||||
of `pid`.
|
||||
|
||||
- If any other term and `pid` is not trapping exits, `pid` will exit with
|
||||
the given `reason`.
|
||||
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
|
||||
the calling process, in which case it will exit with the reason `:normal`).
|
||||
If it is trapping exits, the exit signal is transformed into a message
|
||||
`{:EXIT, from, :normal}` and delivered to its message queue.
|
||||
|
||||
- If any other term and `pid` is trapping exits, the exit signal is
|
||||
transformed into a message `{:EXIT, from, reason}` and delivered to its
|
||||
message queue.
|
||||
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
|
||||
an untrappable exit signal is sent to `pid` which will unconditionally exit
|
||||
with reason `:killed`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -387,6 +388,8 @@ defmodule Process do
|
||||
automatically canceled when `dest` is an atom (as the atom resolution is done
|
||||
on delivery).
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Options
|
||||
|
||||
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
|
||||
@@ -532,7 +535,7 @@ defmodule Process do
|
||||
If the process is already dead when calling `Process.monitor/1`, a
|
||||
`:DOWN` message is delivered immediately.
|
||||
|
||||
See ["Links and monitors"](genservers.md#links-and-monitors)
|
||||
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
|
||||
for an example. See `:erlang.monitor/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -837,7 +840,7 @@ defmodule Process do
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
@spec flag(:save_calls, 0..10_000) :: 0..10_000
|
||||
@spec flag(:save_calls, 0..10000) :: 0..10000
|
||||
@spec flag(:sensitive, boolean) :: boolean
|
||||
@spec flag(:trap_exit, boolean) :: boolean
|
||||
defdelegate flag(flag, value), to: :erlang, as: :process_flag
|
||||
@@ -856,7 +859,7 @@ defmodule Process do
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
|
||||
@spec flag(pid, :save_calls, 0..10000) :: 0..10000
|
||||
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
|
||||
|
||||
@doc """
|
||||
@@ -977,35 +980,11 @@ defmodule Process do
|
||||
@spec unalias(alias) :: boolean
|
||||
defdelegate unalias(alias), to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the label set for the process `pid` as set with `set_label/1`
|
||||
or `:proc_lib.set_label/1`.
|
||||
|
||||
Defaults to the current process when `pid` is not passed.
|
||||
|
||||
## Examples
|
||||
|
||||
Process.set_label({:any, "term"})
|
||||
Process.get_label()
|
||||
#=> {:any, "term"}
|
||||
|
||||
Returns `nil` when not set:
|
||||
|
||||
Process.get_label(pid)
|
||||
#=> nil
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec get_label(pid()) :: term()
|
||||
def get_label(pid \\ self()) do
|
||||
nilify(:proc_lib.get_label(pid))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add a descriptive term to the current process.
|
||||
|
||||
The term does not need to be unique, and will be shown in Observer and in
|
||||
crash logs.
|
||||
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
|
||||
Observer and in crash logs.
|
||||
This label may be useful for identifying a process as one of multiple in a
|
||||
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
|
||||
|
||||
@@ -1019,7 +998,12 @@ defmodule Process do
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec set_label(term()) :: :ok
|
||||
defdelegate set_label(label), to: :proc_lib
|
||||
def set_label(label) do
|
||||
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
|
||||
Process.put(:"$process_label", label)
|
||||
# mimic return value of `:proc_lib.set_label/2`
|
||||
:ok
|
||||
end
|
||||
|
||||
@compile {:inline, nilify: 1}
|
||||
defp nilify(:undefined), do: nil
|
||||
|
||||
+95
-125
@@ -21,7 +21,7 @@ defmodule Protocol do
|
||||
the data structure.
|
||||
|
||||
Although Elixir includes specific functions such as `tuple_size`,
|
||||
`byte_size` and `map_size`, sometimes we want to be able to
|
||||
`binary_size` and `map_size`, sometimes we want to be able to
|
||||
retrieve the size of a data structure regardless of its type.
|
||||
In Elixir we can write polymorphic code, i.e. code that works
|
||||
with different shapes/types, by using protocols. A size protocol
|
||||
@@ -287,11 +287,6 @@ defmodule Protocol do
|
||||
call_args = :lists.map(to_var, :lists.seq(2, arity))
|
||||
call_args = [quote(do: term) | call_args]
|
||||
|
||||
# TODO: Raise in Elixir v2.0
|
||||
if :lists.any(&match?({:\\, _, [_, _]}, &1), args) do
|
||||
IO.warn("default arguments in protocol definitions is deprecated", __CALLER__)
|
||||
end
|
||||
|
||||
quote generated: true do
|
||||
name = unquote(name)
|
||||
arity = unquote(arity)
|
||||
@@ -705,7 +700,7 @@ defmodule Protocol do
|
||||
|
||||
{Descr.term(), clauses, clauses}
|
||||
else
|
||||
{domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
|
||||
{domain, [{[Descr.term()], Descr.atom()}], clauses}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -786,7 +781,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp fallback_clause_for(value, _protocol, meta) do
|
||||
{meta, [{:_, [version: -1], __MODULE__}], [], value}
|
||||
{meta, [quote(do: _)], [], value}
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
@@ -805,7 +800,8 @@ defmodule Protocol do
|
||||
@before_compile Protocol
|
||||
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel, except: [def: 1, def: 2]
|
||||
import Kernel,
|
||||
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
|
||||
|
||||
# Import the new `def` that is used by protocols
|
||||
import Protocol, only: [def: 1]
|
||||
@@ -816,11 +812,12 @@ defmodule Protocol do
|
||||
# Set up a clear slate to store defined functions
|
||||
@__functions__ []
|
||||
@fallback_to_any false
|
||||
@undefined_impl_description ""
|
||||
|
||||
res = unquote(block)
|
||||
# Invoke the user given block
|
||||
_ = unquote(block)
|
||||
|
||||
# Finalize expansion
|
||||
unquote(after_defprotocol())
|
||||
res
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -867,21 +864,6 @@ defmodule Protocol do
|
||||
)
|
||||
end
|
||||
|
||||
extra =
|
||||
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
|
||||
functions) --
|
||||
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
|
||||
|
||||
# TODO: Make an error on Elixir v2.0
|
||||
if extra != [] do
|
||||
warn(
|
||||
"protocols can only define functions without implementation via def/1, found: " <>
|
||||
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
|
||||
env,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
|
||||
@@ -926,105 +908,57 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp after_defprotocol do
|
||||
prefix =
|
||||
quote bind_quoted: [built_in: built_in()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
Protocol.__concat__(__MODULE__, "Any")
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# Disable Dialyzer checks - before and after consolidation
|
||||
# the types could be more strict
|
||||
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
|
||||
|
||||
@doc false
|
||||
@spec impl_for(term) :: atom | nil
|
||||
Kernel.def(impl_for(data))
|
||||
|
||||
# Define the implementation for structs.
|
||||
#
|
||||
# It simply delegates to struct_impl_for which is then
|
||||
# optimized during protocol consolidation.
|
||||
Kernel.def impl_for(%struct{}) do
|
||||
struct_impl_for(struct)
|
||||
quote bind_quoted: [built_in: built_in()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
__MODULE__.Any
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# Define the implementation for built-ins
|
||||
:lists.foreach(
|
||||
fn {mod, guard} ->
|
||||
target = Protocol.__concat__(__MODULE__, mod)
|
||||
# Disable Dialyzer checks - before and after consolidation
|
||||
# the types could be more strict
|
||||
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case Code.ensure_compiled(unquote(target)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
@doc false
|
||||
@spec impl_for(term) :: atom | nil
|
||||
Kernel.def(impl_for(data))
|
||||
|
||||
# Define the implementation for structs.
|
||||
#
|
||||
# It simply delegates to struct_impl_for which is then
|
||||
# optimized during protocol consolidation.
|
||||
Kernel.def impl_for(%struct{}) do
|
||||
struct_impl_for(struct)
|
||||
end
|
||||
|
||||
# Define the implementation for built-ins
|
||||
:lists.foreach(
|
||||
fn {mod, guard} ->
|
||||
target = Protocol.__concat__(__MODULE__, mod)
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case Code.ensure_compiled(unquote(target)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end,
|
||||
built_in
|
||||
)
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
end,
|
||||
built_in
|
||||
)
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@compile {:inline, struct_impl_for: 1}
|
||||
|
||||
if not Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
@typedoc """
|
||||
All the types that implement this protocol.
|
||||
"""
|
||||
@type t :: term
|
||||
end
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
|
||||
@__protocol__ [fallback_to_any: !!@fallback_to_any]
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@spec __protocol__(:consolidated?) :: boolean()
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
|
||||
Kernel.def(__protocol__(:module), do: __MODULE__)
|
||||
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
|
||||
Kernel.def(__protocol__(:consolidated?), do: false)
|
||||
Kernel.def(__protocol__(:impls), do: :not_consolidated)
|
||||
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
|
||||
# destructuring opaque types is illegal, Dialyzer will think none of the
|
||||
# previous clauses matches opaque types, and without this clause, will
|
||||
# conclude that impl_for can't handle an opaque argument). This is a hack
|
||||
# since it relies on Dialyzer not being smart enough to conclude that all
|
||||
# opaque types will get the any_impl_for/0 implementation.
|
||||
Kernel.def impl_for(_) do
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
|
||||
raise =
|
||||
quote do
|
||||
raise(Protocol.UndefinedError,
|
||||
protocol: __MODULE__,
|
||||
value: data,
|
||||
description: @undefined_impl_description
|
||||
)
|
||||
end
|
||||
|
||||
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
|
||||
# destructuring opaque types is illegal, Dialyzer will think none of the
|
||||
# previous clauses matches opaque types, and without this clause, will
|
||||
# conclude that impl_for can't handle an opaque argument). This is a hack
|
||||
# since it relies on Dialyzer not being smart enough to conclude that all
|
||||
# opaque types will get the any_impl_for/0 implementation.
|
||||
impl_for_fallback =
|
||||
quote generated: true, bind_quoted: [] do
|
||||
Kernel.def impl_for(_) do
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
quote generated: true do
|
||||
unquote(prefix)
|
||||
unquote(impl_for_fallback)
|
||||
undefined_impl_description =
|
||||
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
|
||||
|
||||
@doc false
|
||||
@spec impl_for!(term) :: atom
|
||||
@@ -1034,9 +968,47 @@ defmodule Protocol do
|
||||
end
|
||||
else
|
||||
Kernel.def impl_for!(data) do
|
||||
impl_for(data) || unquote(raise)
|
||||
impl_for(data) ||
|
||||
raise(Protocol.UndefinedError,
|
||||
protocol: __MODULE__,
|
||||
value: data,
|
||||
description: unquote(undefined_impl_description)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@compile {:inline, struct_impl_for: 1}
|
||||
|
||||
if not Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
@typedoc """
|
||||
All the types that implement this protocol.
|
||||
"""
|
||||
@type t :: term
|
||||
end
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
|
||||
@__protocol__ [fallback_to_any: !!@fallback_to_any]
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@spec __protocol__(:consolidated?) :: boolean()
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
|
||||
Kernel.def(__protocol__(:module), do: __MODULE__)
|
||||
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
|
||||
Kernel.def(__protocol__(:consolidated?), do: false)
|
||||
Kernel.def(__protocol__(:impls), do: :not_consolidated)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1223,12 +1195,10 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __concat__(left, right) when is_atom(right) do
|
||||
__concat__(left, remove_prefix(Atom.to_string(right)))
|
||||
end
|
||||
|
||||
def __concat__(left, right) when is_binary(right) do
|
||||
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
|
||||
def __concat__(left, right) do
|
||||
String.to_atom(
|
||||
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
|
||||
)
|
||||
end
|
||||
|
||||
defp ensure_prefix("Elixir." <> _ = left), do: left
|
||||
|
||||
@@ -207,6 +207,12 @@ defmodule Range do
|
||||
%Range{first: first, last: last, step: step}
|
||||
end
|
||||
|
||||
def new(first, last) do
|
||||
raise ArgumentError,
|
||||
"ranges (first..last) expect both sides to be integers, " <>
|
||||
"got: #{inspect(first)}..#{inspect(last)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new range with `step`.
|
||||
|
||||
@@ -284,12 +290,9 @@ defmodule Range do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec shift(t, integer) :: t
|
||||
def shift(%Range{} = range, 0), do: range
|
||||
|
||||
def shift(first..last//step, steps_to_shift) when is_integer(steps_to_shift) do
|
||||
shift = steps_to_shift * step
|
||||
|
||||
new(first + shift, last + shift, step)
|
||||
def shift(first..last//step, steps_to_shift)
|
||||
when is_integer(steps_to_shift) do
|
||||
new(first + steps_to_shift * step, last + steps_to_shift * step, step)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
+5
-33
@@ -78,7 +78,8 @@ defmodule Regex do
|
||||
|
||||
* `:caseless` (i) - adds case insensitivity
|
||||
|
||||
* `:dotall` (s) - causes dot to match newlines and also sets newline to `(*ANYCRLF)`.
|
||||
* `:dotall` (s) - causes dot to match newlines and also sets newline to
|
||||
`(*ANYCRLF)`.\
|
||||
The new line setting, as described in the [`:re` documentation](`:re`),
|
||||
can be overridden by starting the regular expression pattern with:
|
||||
* `(*CR)` - carriage return
|
||||
@@ -86,7 +87,7 @@ defmodule Regex do
|
||||
* `(*CRLF)` - carriage return, followed by line feed
|
||||
* `(*ANYCRLF)` - any of the three above
|
||||
* `(*ANY)` - all Unicode newline sequences
|
||||
* `(*NUL)` - the NUL character (binary zero) (from Erlang/OTP 28)
|
||||
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
|
||||
|
||||
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
|
||||
each line; use `\A` and `\z` to match the end or beginning of the string
|
||||
@@ -101,8 +102,8 @@ defmodule Regex do
|
||||
(the previous `r` option is deprecated in favor of `U`)
|
||||
|
||||
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
|
||||
which can be shared across nodes or passed through config, at the cost of a runtime
|
||||
overhead to re-import it every time it is executed.
|
||||
which can be shared across nodes or through config, at the cost of a runtime
|
||||
overhead every time to re-import it every time it is executed.
|
||||
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
|
||||
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
|
||||
and do not need to be exported in order to be shared in these versions.
|
||||
@@ -274,35 +275,6 @@ defmodule Regex do
|
||||
regex
|
||||
end
|
||||
|
||||
@doc """
|
||||
Imports a `regex` that has been exported, otherwise returns the `regex` unchanged.
|
||||
|
||||
This means it will lose the ability to be sent across nodes or passed through config,
|
||||
but will be faster since it won't need to be imported on the fly every time it is executed.
|
||||
|
||||
Exported regexes only exist on OTP 28, so this has no effect on older versions.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.import(~r/foo/E)
|
||||
~r/foo/
|
||||
|
||||
Regex.import(~r/foo/)
|
||||
~r/foo/
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec import(t) :: t
|
||||
def import(%Regex{re_pattern: re_pattern} = regex) do
|
||||
case re_pattern do
|
||||
{:re_exported_pattern, _, _, _, _} ->
|
||||
%{regex | re_pattern: :re.import(re_pattern), opts: regex.opts -- [:export]}
|
||||
|
||||
_ ->
|
||||
regex
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the version of the underlying Regex engine.
|
||||
"""
|
||||
|
||||
+88
-204
@@ -258,7 +258,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
{pid, _} ->
|
||||
if Process.alive?(pid), do: pid, else: :undefined
|
||||
|
||||
@@ -346,8 +346,7 @@ defmodule Registry do
|
||||
|
||||
The registry requires the following keys:
|
||||
|
||||
* `:keys` - chooses if keys are `:unique`, `:duplicate`,
|
||||
`{:duplicate, :key}`, or `{:duplicate, :pid}`
|
||||
* `:keys` - chooses if keys are `:unique`, `:duplicate`, `{:duplicate, :key}`, or `{:duplicate, :pid}`
|
||||
* `:name` - the name of the registry and its tables
|
||||
|
||||
The following keys are optional:
|
||||
@@ -359,20 +358,17 @@ defmodule Registry do
|
||||
crashes. Messages sent to listeners are of type `t:listener_message/0`.
|
||||
* `:meta` - a keyword list of metadata to be attached to the registry.
|
||||
|
||||
For `:duplicate` registries, you can specify the partitioning strategy
|
||||
directly in the `:keys` option:
|
||||
For `:duplicate` registries, you can specify the partitioning strategy
|
||||
directly in the `:keys` option:
|
||||
|
||||
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
|
||||
when you have keys with many entries (e.g., one topic with many subscribers).
|
||||
This is the traditional behavior and groups all entries from the same process together.
|
||||
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
|
||||
when you have keys with many entries (e.g., one topic with many subscribers).
|
||||
This is the traditional behavior and groups all entries from the same process together.
|
||||
|
||||
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
|
||||
many different keys (e.g., many topics with few subscribers each). This makes
|
||||
key-based lookups more efficient as they only need to check a single partition
|
||||
instead of all partitions. 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.
|
||||
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
|
||||
many different keys (e.g., many topics with few subscribers each). This makes
|
||||
key-based lookups more efficient as they only need to check a single partition
|
||||
instead of all partitions.
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@@ -535,17 +531,13 @@ 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. Only meaningful for `{:duplicate, :pid}`
|
||||
registries. Defaults to `false`.
|
||||
across all partitions. Defaults to `false`.
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@@ -556,24 +548,17 @@ 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 || key_ets!(registry, key, partitions)
|
||||
|
||||
:unique
|
||||
|> lookup_second(key_ets, key)
|
||||
(key_ets || key_ets!(registry, key, partitions))
|
||||
|> safe_lookup_second(key)
|
||||
|> List.wrap()
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
kind
|
||||
|> lookup_second(key_ets, key)
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
key_ets
|
||||
|> safe_lookup_second(key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{: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, _} ->
|
||||
{{:duplicate, _}, partitions, _} ->
|
||||
if Keyword.get(opts, :parallel, false) do
|
||||
registry
|
||||
|> dispatch_parallel(key, mfa_or_fun, partitions)
|
||||
@@ -593,8 +578,9 @@ defmodule Registry do
|
||||
defp dispatch_serial(registry, key, mfa_or_fun, partition) do
|
||||
partition = partition - 1
|
||||
|
||||
{:duplicate, :pid}
|
||||
|> lookup_second(key_ets!(registry, partition), key)
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
dispatch_serial(registry, key, mfa_or_fun, partition)
|
||||
@@ -610,8 +596,9 @@ defmodule Registry do
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
{:duplicate, :pid}
|
||||
|> lookup_second(key_ets!(registry, partition), key)
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
Process.unlink(parent)
|
||||
@@ -675,7 +662,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
{_, _} = pair ->
|
||||
[pair]
|
||||
|
||||
@@ -683,15 +670,16 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
lookup_second(kind, key_ets, key)
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
safe_lookup_second(key_ets, key)
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key)
|
||||
partition = hash(key, partitions)
|
||||
safe_lookup_second(key_ets!(registry, partition), key)
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key),
|
||||
pair <- safe_lookup_second(key_ets!(registry, partition), key),
|
||||
do: pair
|
||||
end
|
||||
end
|
||||
@@ -802,18 +790,21 @@ 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
|
||||
case key_info!(registry) do
|
||||
{{:duplicate, :pid}, partitions, nil} ->
|
||||
spec = match_spec({:duplicate, :pid}, key, pattern, guards)
|
||||
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, _}, 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
|
||||
|
||||
@@ -951,7 +942,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
{^pid, value} ->
|
||||
[value]
|
||||
|
||||
@@ -959,17 +950,18 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
for {^pid, value} <- lookup_second(kind, key_ets, key), do: value
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
key_ets = key_ets!(registry, key, partitions)
|
||||
for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value
|
||||
partition = hash(key, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
partition = hash(pid, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1023,8 +1015,7 @@ 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, ordered?(kind))
|
||||
|
||||
true = __unregister__(key_ets, {key, {self, :_}}, 1)
|
||||
true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2)
|
||||
|
||||
unlink_if_unregistered(pid_server, pid_ets, self)
|
||||
@@ -1089,10 +1080,13 @@ 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.
|
||||
{total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards)
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
|
||||
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 ->
|
||||
@@ -1126,25 +1120,6 @@ 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`.
|
||||
|
||||
@@ -1204,14 +1179,7 @@ defmodule Registry do
|
||||
counter = System.unique_integer()
|
||||
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
|
||||
|
||||
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
|
||||
case register_key(kind, key_ets, key, {key, {self, value}}) do
|
||||
:ok ->
|
||||
for listener <- listeners do
|
||||
Kernel.send(listener, {:register, registry, key, self, value})
|
||||
@@ -1440,18 +1408,21 @@ 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
|
||||
case key_info!(registry) do
|
||||
{{:duplicate, :pid}, partitions, nil} ->
|
||||
spec = count_match_spec({:duplicate, :pid}, key, pattern, guards)
|
||||
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, _}, 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
|
||||
|
||||
@@ -1480,9 +1451,6 @@ 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.
|
||||
@@ -1510,16 +1478,15 @@ defmodule Registry do
|
||||
@spec select(registry, spec) :: [term]
|
||||
def select(registry, spec)
|
||||
when is_atom(registry) and is_list(spec) do
|
||||
case key_info!(registry) do
|
||||
{kind, partitions, nil} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
spec = group_match_headers(spec, __ENV__.function)
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
Enum.flat_map(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{kind, 1, key_ets} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
{_kind, 1, key_ets} ->
|
||||
:ets.select(key_ets, spec)
|
||||
end
|
||||
end
|
||||
@@ -1542,30 +1509,24 @@ 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
|
||||
case key_info!(registry) do
|
||||
{kind, partitions, nil} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
spec = group_match_headers(spec, __ENV__.function)
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{kind, 1, key_ets} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
{_kind, 1, key_ets} ->
|
||||
:ets.select_count(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_match_headers(spec, kind, {fun, arity}) do
|
||||
ordered = ordered?(kind)
|
||||
|
||||
defp group_match_headers(spec, {fun, arity}) do
|
||||
for part <- spec do
|
||||
case part do
|
||||
{{key, pid, value}, guards, body} when ordered ->
|
||||
{{{key, pid, :_}, value}, guards, body}
|
||||
|
||||
{{key, pid, value}, guards, body} ->
|
||||
{{key, {pid, value}}, guards, body}
|
||||
{{key, pid, value}, guards, select} ->
|
||||
{{key, {pid, value}}, guards, select}
|
||||
|
||||
_ ->
|
||||
raise ArgumentError,
|
||||
@@ -1616,23 +1577,7 @@ defmodule Registry do
|
||||
:ets.lookup_element(registry, partition, 3)
|
||||
end
|
||||
|
||||
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
|
||||
defp safe_lookup_second(ets, key) do
|
||||
try do
|
||||
:ets.lookup_element(ets, key, 2)
|
||||
catch
|
||||
@@ -1661,22 +1606,10 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc false
|
||||
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
|
||||
def __unregister__(table, match, pos) do
|
||||
key = :erlang.element(pos, match)
|
||||
|
||||
# We need to perform an element comparison if we have a special atom key.
|
||||
# We need to perform an element comparison if we have an special atom key.
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
match = :erlang.setelement(pos, match, :_)
|
||||
guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}}
|
||||
@@ -1686,52 +1619,9 @@ 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
|
||||
@@ -1843,8 +1733,7 @@ defmodule Registry.Partition do
|
||||
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
|
||||
end
|
||||
|
||||
ordered = kind == {:duplicate, :key}
|
||||
{:ok, {pid_ets, %{}, ordered}}
|
||||
{:ok, {pid_ets, %{}}}
|
||||
end
|
||||
|
||||
# The key partition is a set for unique keys,
|
||||
@@ -1854,12 +1743,7 @@ defmodule Registry.Partition do
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
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
|
||||
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
|
||||
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
@@ -1883,7 +1767,7 @@ defmodule Registry.Partition do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:lock, key}, from, {ets, lock, ordered}) do
|
||||
def handle_call({:lock, key}, from, {ets, lock}) do
|
||||
lock =
|
||||
case lock do
|
||||
%{^key => queue} ->
|
||||
@@ -1894,10 +1778,10 @@ defmodule Registry.Partition do
|
||||
Map.put(lock, key, :queue.new())
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
{:noreply, {ets, lock}}
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
|
||||
entries = :ets.take(ets, pid)
|
||||
|
||||
for {_pid, key, key_ets, _counter} <- entries do
|
||||
@@ -1912,13 +1796,13 @@ defmodule Registry.Partition do
|
||||
end
|
||||
|
||||
try do
|
||||
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered)
|
||||
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1)
|
||||
catch
|
||||
:error, :badarg -> :badarg
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
{:noreply, {ets, lock}}
|
||||
end
|
||||
|
||||
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
|
||||
@@ -1930,7 +1814,7 @@ defmodule Registry.Partition do
|
||||
unlock(key, state)
|
||||
end
|
||||
|
||||
defp unlock(key, {ets, lock, ordered}) do
|
||||
defp unlock(key, {ets, lock}) do
|
||||
%{^key => queue} = lock
|
||||
|
||||
lock =
|
||||
@@ -1939,7 +1823,7 @@ defmodule Registry.Partition do
|
||||
{:not_empty, queue} -> Map.put(lock, key, queue)
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
{:noreply, {ets, lock}}
|
||||
end
|
||||
|
||||
defp dequeue(queue, key) do
|
||||
|
||||
@@ -1437,7 +1437,7 @@ defmodule Stream do
|
||||
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
|
||||
|
||||
{_, []} ->
|
||||
do_cycle(check_cycle_subsequent_element(cycle), [], cycle, {:cont, acc}, fun)
|
||||
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1446,26 +1446,10 @@ 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 exception, message
|
||||
raise ArgumentError, "cannot cycle over an empty enumerable"
|
||||
|
||||
other ->
|
||||
other
|
||||
|
||||
+12
-64
@@ -22,7 +22,7 @@ defmodule String do
|
||||
"hello world"
|
||||
|
||||
The functions in this module act according to
|
||||
[The Unicode Standard, Version 17.0.0](https://www.unicode.org/versions/Unicode17.0.0/).
|
||||
[The Unicode Standard, Version 17.0.0](http://www.unicode.org/versions/Unicode17.0.0/).
|
||||
|
||||
## Interpolation
|
||||
|
||||
@@ -483,7 +483,7 @@ defmodule String do
|
||||
iex> String.split("abc", "", parts: 3)
|
||||
["", "a", "bc"]
|
||||
|
||||
Splitting on a non-existing pattern returns the original string:
|
||||
Splitting on an non-existing pattern returns the original string:
|
||||
|
||||
iex> String.split("abc", ",")
|
||||
["abc"]
|
||||
@@ -745,15 +745,7 @@ defmodule String do
|
||||
iex> String.count("hello world", "")
|
||||
12
|
||||
|
||||
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:
|
||||
The `pattern` can also be a compiled pattern:
|
||||
|
||||
iex> pattern = :binary.compile_pattern([" ", "!"])
|
||||
iex> String.count("foo bar baz!!", pattern)
|
||||
@@ -764,8 +756,6 @@ 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
|
||||
@@ -1008,10 +998,6 @@ defmodule String do
|
||||
IO.iodata_to_binary(downcase_ascii(string))
|
||||
end
|
||||
|
||||
def downcase(string, :greek) when is_binary(string) do
|
||||
String.Unicode.downcase_greek(string, [])
|
||||
end
|
||||
|
||||
def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
|
||||
String.Unicode.downcase(string, [], mode)
|
||||
end
|
||||
@@ -1062,39 +1048,19 @@ defmodule String do
|
||||
@letter_I_dot_above <<0x0130::utf8>>
|
||||
|
||||
def capitalize(<<@letter_i, right::binary>>, mode) do
|
||||
capitalized = if mode == :turkic, do: @letter_I_dot_above, else: @letter_I
|
||||
capitalized <> downcase_after_capitalize(right, mode, ?i)
|
||||
if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode)
|
||||
end
|
||||
|
||||
def capitalize(string, mode) when is_binary(string) do
|
||||
case :unicode_util.gc(string) do
|
||||
[gc] ->
|
||||
grapheme_to_binary(:string.titlecase([gc]))
|
||||
|
||||
[gc, rest] ->
|
||||
grapheme_to_binary(:string.titlecase([gc])) <>
|
||||
downcase_after_capitalize(rest, mode, gc)
|
||||
|
||||
[gc | rest] ->
|
||||
grapheme_to_binary(:string.titlecase([gc])) <>
|
||||
downcase_after_capitalize(rest, mode, gc)
|
||||
|
||||
[] ->
|
||||
""
|
||||
|
||||
{:error, <<byte, rest::bits>>} ->
|
||||
<<byte>> <> downcase(rest, mode)
|
||||
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
|
||||
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[] -> ""
|
||||
{:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode)
|
||||
end
|
||||
end
|
||||
|
||||
defp downcase_after_capitalize(rest, :greek, gc) do
|
||||
String.Unicode.downcase_greek(rest, [], grapheme_to_binary(gc))
|
||||
end
|
||||
|
||||
defp downcase_after_capitalize(rest, mode, _gc) do
|
||||
downcase(rest, mode)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.trim_trailing/1 instead"
|
||||
defdelegate rstrip(binary), to: String.Break, as: :trim_trailing
|
||||
@@ -2324,19 +2290,6 @@ 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)
|
||||
@@ -3215,7 +3168,8 @@ defmodule String do
|
||||
def jaro_distance("", _string), do: 0.0
|
||||
|
||||
def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
:string.jaro_similarity(string1, string2)
|
||||
# TODO: Replace by :string.jaro_similarity/2 when we require Erlang/OTP 27+
|
||||
:elixir_utils.jaro_similarity(string1, string2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -3260,13 +3214,7 @@ defmodule String do
|
||||
byte_size_unicode(unicode)
|
||||
end
|
||||
|
||||
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
|
||||
defp byte_size_remaining_at(unicode, n) do
|
||||
case :unicode_util.gc(unicode) do
|
||||
[_] -> 0
|
||||
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
|
||||
|
||||
@@ -556,29 +556,19 @@ 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`.
|
||||
|
||||
See the documentation for those functions for more information.
|
||||
"""
|
||||
@typedoc "Return values of `start_link/2` and `start_link/3`."
|
||||
@type on_start ::
|
||||
{:ok, pid}
|
||||
| :ignore
|
||||
| {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
|
||||
@typedoc """
|
||||
Return values of `start_child/2`.
|
||||
|
||||
See the documentation for those functions for more information.
|
||||
"""
|
||||
@typedoc "Return values of `start_child/2`."
|
||||
@type on_start_child ::
|
||||
{:ok, child}
|
||||
| {:ok, child, info :: term}
|
||||
@@ -708,18 +698,15 @@ 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.
|
||||
`{: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.
|
||||
|
||||
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}}}`.
|
||||
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}}`.
|
||||
|
||||
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
|
||||
@@ -952,17 +939,9 @@ 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 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}}}`.
|
||||
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`.
|
||||
|
||||
The `:name` option can also be given in order to register a supervisor
|
||||
name, the supported values are described in the "Name registration"
|
||||
@@ -1132,8 +1111,7 @@ defmodule Supervisor do
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
|
||||
Note that calling this function when supervising a large number of children
|
||||
under low memory conditions can bring the system down due to an out of memory
|
||||
error.
|
||||
under low memory conditions can cause an out of memory exception.
|
||||
|
||||
This function returns a list of `{id, child, type, modules}` tuples, where:
|
||||
|
||||
|
||||
+19
-20
@@ -1040,6 +1040,25 @@ defmodule System do
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> System.cmd("echo", ["hello"])
|
||||
{"hello\n", 0}
|
||||
|
||||
iex> System.cmd("echo", ["hello"], env: [{"MIX_ENV", "test"}])
|
||||
{"hello\n", 0}
|
||||
|
||||
If you want to stream the output to Standard IO as it arrives:
|
||||
|
||||
iex> System.cmd("echo", ["hello"], into: IO.stream())
|
||||
hello
|
||||
{%IO.Stream{}, 0}
|
||||
|
||||
If you want to read lines:
|
||||
|
||||
iex> System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
|
||||
{["hello", "world"], 0}
|
||||
|
||||
## Options
|
||||
|
||||
* `:into` - injects the result into the given collectable, defaults to `""`
|
||||
@@ -1103,26 +1122,6 @@ defmodule System do
|
||||
|
||||
If you desire to execute a trusted command inside a shell, with pipes,
|
||||
redirecting and so on, please check `shell/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> System.cmd("echo", ["hello"])
|
||||
{"hello\n", 0}
|
||||
|
||||
iex> System.cmd("echo", ["hello"], env: [{"MIX_ENV", "test"}])
|
||||
{"hello\n", 0}
|
||||
|
||||
If you want to stream the output to Standard IO as it arrives:
|
||||
|
||||
iex> System.cmd("echo", ["hello"], into: IO.stream())
|
||||
hello
|
||||
{%IO.Stream{}, 0}
|
||||
|
||||
If you want to read lines:
|
||||
|
||||
iex> System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
|
||||
{["hello", "world"], 0}
|
||||
|
||||
"""
|
||||
@spec cmd(binary, [binary], cmd_opts) :: {Collectable.t(), exit_status :: non_neg_integer}
|
||||
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
|
||||
|
||||
+7
-10
@@ -105,7 +105,7 @@ defmodule Task do
|
||||
], strategy: :one_for_one)
|
||||
|
||||
And now you can use async/await by passing the name of
|
||||
the supervisor instead of the PID:
|
||||
the supervisor instead of the pid:
|
||||
|
||||
Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
|
||||
# Do something
|
||||
@@ -165,7 +165,7 @@ defmodule Task do
|
||||
{Task, fn -> :some_work end}
|
||||
], strategy: :one_for_one)
|
||||
|
||||
This is often useful when you need to execute code concurrently while
|
||||
This is often useful when you need to execute some steps while
|
||||
setting up your supervision tree. For example: to warm up caches,
|
||||
log the initialization status, and such.
|
||||
|
||||
@@ -196,17 +196,15 @@ defmodule Task do
|
||||
and `Task.start_link/1` are for fire-and-forget tasks, where you don't
|
||||
care about the results or if it completes successfully or not.
|
||||
|
||||
Keep in mind the Supervisor will not wait for the task to finish running
|
||||
before starting the next child or returning. If you need synchronous
|
||||
initialization, then either use an `Agent` or a `GenServer`.
|
||||
|
||||
> #### `use Task` {: .info}
|
||||
>
|
||||
> When you `use Task`, the `Task` module will define a
|
||||
> `child_spec/1` function, so your module can be used
|
||||
> as a child in a supervision tree.
|
||||
|
||||
The generated `child_spec/1` can be customized with the following options:
|
||||
`use Task` defines a `child_spec/1` function, allowing the
|
||||
defined module to be put under a supervision tree. The generated
|
||||
`child_spec/1` can be customized with the following options:
|
||||
|
||||
* `:id` - the child specification identifier, defaults to the current module
|
||||
* `:restart` - when the child should be restarted, defaults to `:temporary`
|
||||
@@ -230,7 +228,7 @@ defmodule Task do
|
||||
|
||||
## Ancestor and Caller Tracking
|
||||
|
||||
Whenever you start a new process, Elixir annotates the process with the parent
|
||||
Whenever you start a new process, Elixir annotates the parent of that process
|
||||
through the `$ancestors` key in the process dictionary. This is often used to
|
||||
track the hierarchy inside a supervision tree.
|
||||
|
||||
@@ -1248,10 +1246,9 @@ defmodule Task do
|
||||
{ref, nil}
|
||||
end)
|
||||
|
||||
ref_count = map_size(refs)
|
||||
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
|
||||
timeout = Keyword.get(opts, :timeout, 5_000)
|
||||
limit = min(Keyword.get(opts, :limit, ref_count), ref_count)
|
||||
limit = Keyword.get(opts, :limit, map_size(refs))
|
||||
timeout_ref = make_ref()
|
||||
|
||||
timer_ref =
|
||||
|
||||
@@ -54,7 +54,7 @@ defmodule Task.Supervised do
|
||||
# 4. The calling process has not exited and so does not send its monitor reference
|
||||
# 5. The spawned task waits forever for the monitor reference so it can begin
|
||||
#
|
||||
# We have solved this by specifying a timeout of 5000 milliseconds.
|
||||
# We have solved this by specifying a timeout of 5000 seconds.
|
||||
# Given no work is done in the client between the task start and
|
||||
# sending the reference, 5000 should be enough to not raise false
|
||||
# negatives unless the nodes are indeed not available.
|
||||
@@ -122,8 +122,8 @@ defmodule Task.Supervised do
|
||||
function: fun,
|
||||
args: args,
|
||||
reason: {log_value(kind, value), __STACKTRACE__},
|
||||
# using :proc_lib over Process because we want the :undefined default, not nil
|
||||
process_label: :proc_lib.get_label(self())
|
||||
# TODO use Process.get_label/0 when we require Erlang/OTP 27+
|
||||
process_label: Process.get(:"$process_label", :undefined)
|
||||
}
|
||||
},
|
||||
%{
|
||||
@@ -162,7 +162,7 @@ defmodule Task.Supervised do
|
||||
~c"** When function == ~p~n" ++
|
||||
~c"** arguments == ~p~n" ++ ~c"** Reason for termination == ~n" ++ ~c"** ~p~n"
|
||||
|
||||
terms = [starter, fun, args, get_reason(reason)]
|
||||
terms = [name, fun, args, get_reason(reason)]
|
||||
|
||||
{message, terms} =
|
||||
case process_label do
|
||||
@@ -173,7 +173,7 @@ defmodule Task.Supervised do
|
||||
message =
|
||||
~c"** Task ~p terminating~n" ++ message
|
||||
|
||||
{message, [name | terms]}
|
||||
{message, [starter | terms]}
|
||||
end
|
||||
|
||||
defp get_from({node, pid_or_name, _pid}) when node == node(), do: pid_or_name
|
||||
|
||||
@@ -502,8 +502,8 @@ defmodule Task.Supervisor do
|
||||
Returns all children PIDs except those that are restarting.
|
||||
|
||||
Note that calling this function when supervising a large number
|
||||
of children under low memory conditions can bring the system down due to an
|
||||
out of memory error.
|
||||
of children under low memory conditions can cause an out of memory
|
||||
exception.
|
||||
"""
|
||||
@spec children(Supervisor.supervisor()) :: [pid]
|
||||
def children(supervisor) do
|
||||
|
||||
+17
-53
@@ -8,12 +8,10 @@ defmodule URI do
|
||||
|
||||
This module provides functions for working with URIs (for example, parsing
|
||||
URIs or encoding query strings). The functions in this module are implemented
|
||||
according to [RFC 3986](https://tools.ietf.org/html/rfc3986) and it also
|
||||
provides additional functionality for handling "application/x-www-form-urlencoded"
|
||||
segments.
|
||||
according to [RFC 3986](https://tools.ietf.org/html/rfc3986).
|
||||
|
||||
Additionally, the Erlang [`:uri_string` module](`:uri_string`) provides additional
|
||||
functionality such as RFC 3986 compliant URI normalization.
|
||||
Additionally, the Erlang [`:uri_string` module](`:uri_string`) provides certain functionalities,
|
||||
such as RFC 3986 compliant URI normalization.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
@@ -24,12 +22,6 @@ defmodule URI do
|
||||
|
||||
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
|
||||
|
||||
The fields contain the encoded URI components as they appear in the URI
|
||||
itself. For example, a slash inside the userinfo must be stored as `%2F`,
|
||||
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
|
||||
percent-encoded sequences in those fields, and functions such as `to_string/1`
|
||||
expects those fields to already be encoded as needed. Whenever setting or
|
||||
modifying the fields directly, you must encode them accordingly.
|
||||
|
||||
Note the `authority` field is deprecated. `parse/1` will still
|
||||
populate it for backwards compatibility but you should generally
|
||||
@@ -38,11 +30,6 @@ defmodule URI do
|
||||
@derive {Inspect, optional: [:authority]}
|
||||
defstruct [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment]
|
||||
|
||||
# We don't use opaque because URIs can be inlined,
|
||||
# either via module attributes or by the compiler.
|
||||
@typedoc deprecated: "The authority field is deprecated"
|
||||
@type authority :: nil | binary
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
scheme: nil | binary,
|
||||
authority: authority,
|
||||
@@ -54,6 +41,9 @@ defmodule URI do
|
||||
fragment: nil | binary
|
||||
}
|
||||
|
||||
@typedoc deprecated: "The authority field is deprecated"
|
||||
@opaque authority :: nil | binary
|
||||
|
||||
defmodule Error do
|
||||
@moduledoc """
|
||||
An exception raised when an error occurs when a `URI` is invalid.
|
||||
@@ -511,9 +501,9 @@ defmodule URI do
|
||||
defp hex_to_dec(_n), do: nil
|
||||
|
||||
@doc """
|
||||
Creates a new URI struct by parsing and validating a string or from an existing URI.
|
||||
Creates a new URI struct from a URI or a string.
|
||||
|
||||
If a `%URI{}` struct is given, it returns `{:ok, uri}` as is. If a string is
|
||||
If a `%URI{}` struct is given, it returns `{:ok, uri}`. If a string is
|
||||
given, it will parse and validate it. If the string is valid, it returns
|
||||
`{:ok, uri}`, otherwise it returns `{:error, part}` with the invalid part
|
||||
of the URI. For parsing URIs without further validation, see `parse/1`.
|
||||
@@ -526,16 +516,6 @@ defmodule URI do
|
||||
for the URI's scheme is used for the `:port` field. The scheme is also
|
||||
normalized to lowercase.
|
||||
|
||||
> #### Browser compatibility {: .warning}
|
||||
>
|
||||
> This function does not follow the same parsing rules as browsers.
|
||||
> Browsers adhere to the WHATWG URL standard, while this function
|
||||
> implements RFC 3986. You should expect different behaviours between
|
||||
> them and this function will often reject URLs that are accepted by
|
||||
> browsers (as they tend to be permissive). Common examples include
|
||||
> unencoded square brackets in query strings (such as `foo[bar]=baz`)
|
||||
> and backslashes used as path separators.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> URI.new("https://elixir-lang.org/")
|
||||
@@ -709,26 +689,18 @@ defmodule URI do
|
||||
|
||||
This function can parse both absolute and relative URLs. You can check
|
||||
if a URI is absolute or relative by checking if the `scheme` field is
|
||||
nil or not.
|
||||
|
||||
This function expects both absolute and relative URIs to be well-formed
|
||||
and does not perform any validation, it simply breaks the URL into parts.
|
||||
Use `new/1` if you want to validate the URI fields after parsing.
|
||||
nil or not. Furthermore, this function expects both absolute and
|
||||
relative URIs to be well-formed and does not perform any validation.
|
||||
See the "Examples" section below. Use `new/1` if you want to validate
|
||||
the URI fields after parsing.
|
||||
|
||||
When a URI is given without a port, the value returned by `URI.default_port/1`
|
||||
for the URI's scheme is used for the `:port` field. The scheme is also
|
||||
normalized to lowercase.
|
||||
|
||||
If a `URI` struct is given to this function, this function returns it
|
||||
If a `%URI{}` struct is given to this function, this function returns it
|
||||
unmodified.
|
||||
|
||||
> #### Browser compatibility {: .warning}
|
||||
>
|
||||
> This function does not follow the same parsing rules as browsers.
|
||||
> Browsers adhere to the WHATWG URL standard, while this function
|
||||
> implements RFC 3986. You should expect different behaviours between
|
||||
> them, especially around corner cases.
|
||||
|
||||
> #### `:authority` field {: .info}
|
||||
>
|
||||
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
|
||||
@@ -781,9 +753,8 @@ defmodule URI do
|
||||
userinfo: nil
|
||||
}
|
||||
|
||||
In contrast to `URI.new/1`, this function will accept poorly-formed
|
||||
URIs and break them into components, as it does not perform validation.
|
||||
For example:
|
||||
In contrast to `URI.new/1`, this function will parse poorly-formed
|
||||
URIs, for example:
|
||||
|
||||
iex> URI.parse("/invalid_greater_than_in_path/>")
|
||||
%URI{
|
||||
@@ -797,8 +768,8 @@ defmodule URI do
|
||||
}
|
||||
|
||||
Another example is a URI with brackets in query strings. It is accepted
|
||||
by `parse/1` for the same reasons as above (no validation), but it will
|
||||
be refused by `new/1`:
|
||||
by `parse/1`, it is commonly accepted by browsers, but it will be refused
|
||||
by `new/1`:
|
||||
|
||||
iex> URI.parse("/?foo[bar]=baz")
|
||||
%URI{
|
||||
@@ -811,9 +782,6 @@ defmodule URI do
|
||||
userinfo: nil
|
||||
}
|
||||
|
||||
Generally speaking, square brackets are strictly invalid even in the WHATWG
|
||||
URL standard used by browsers, but browsers tend to be permissive and often
|
||||
allow invalid characters to pass through.
|
||||
"""
|
||||
@spec parse(t | binary) :: t
|
||||
def parse(%URI{} = uri), do: uri
|
||||
@@ -903,9 +871,6 @@ defmodule URI do
|
||||
@doc """
|
||||
Returns the string representation of the given [URI struct](`t:t/0`).
|
||||
|
||||
This function assembles the URI components into a string, assuming each
|
||||
field is valid and escaped as done by `parse/1` and `new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> uri = URI.parse("http://google.com")
|
||||
@@ -973,7 +938,6 @@ defmodule URI do
|
||||
end
|
||||
|
||||
defp merge_paths(nil, rel_path), do: merge_paths("/", rel_path)
|
||||
defp merge_paths("", rel_path), do: merge_paths("/", rel_path)
|
||||
defp merge_paths(_, "/" <> _ = rel_path), do: remove_dot_segments_from_path(rel_path)
|
||||
|
||||
defp merge_paths(base_path, rel_path) do
|
||||
|
||||
@@ -18,16 +18,12 @@ defmodule Version do
|
||||
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
Each numeric component is limited to at most 14 digits.
|
||||
|
||||
Pre-releases are supported by optionally appending a hyphen and a series of
|
||||
period-separated identifiers immediately following the patch version.
|
||||
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
|
||||
|
||||
"1.0.0-alpha.3"
|
||||
|
||||
Numeric pre-release identifiers are also limited to at most 14 digits.
|
||||
|
||||
Build information can be added by appending a plus sign and a series of
|
||||
dot-separated identifiers immediately following the patch or pre-release version.
|
||||
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
|
||||
@@ -48,11 +44,6 @@ defmodule Version do
|
||||
# Anything later than 2.0.0
|
||||
"> 2.0.0"
|
||||
|
||||
You can skip the operator, which is equivalent to `==`:
|
||||
|
||||
# Only version 2.0.0
|
||||
"2.0.0"
|
||||
|
||||
Requirements also support `and` and `or` for complex conditions:
|
||||
|
||||
# 2.0.0 and later until 2.1.0
|
||||
@@ -524,8 +515,6 @@ defmodule Version do
|
||||
defmodule Parser do
|
||||
@moduledoc false
|
||||
|
||||
@max_numeric_component_digits 14
|
||||
|
||||
operators = [
|
||||
{">=", :>=},
|
||||
{"<=", :<=},
|
||||
@@ -627,9 +616,7 @@ defmodule Version do
|
||||
defp require_digits(nil), do: :error
|
||||
|
||||
defp require_digits(string) do
|
||||
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
|
||||
do: :error,
|
||||
else: parse_digits(string, "")
|
||||
if leading_zero?(string), do: :error, else: parse_digits(string, "")
|
||||
end
|
||||
|
||||
defp leading_zero?(<<?0, _, _::binary>>), do: true
|
||||
@@ -657,11 +644,6 @@ defmodule Version do
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_parts_to_integer([part | rest], acc)
|
||||
when byte_size(part) > @max_numeric_component_digits do
|
||||
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
|
||||
end
|
||||
|
||||
defp convert_parts_to_integer([part | rest], acc) do
|
||||
case parse_digits(part, "") do
|
||||
{:ok, integer} ->
|
||||
@@ -680,10 +662,6 @@ defmodule Version do
|
||||
{:ok, Enum.reverse(acc)}
|
||||
end
|
||||
|
||||
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
|
||||
defp all_digits?(<<>>), do: true
|
||||
defp all_digits?(_other), do: false
|
||||
|
||||
defp valid_identifier?(<<char, rest::binary>>)
|
||||
when char in ?0..?9
|
||||
when char in ?a..?z
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user