Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
* Describe here the reasons behind the pull request.
|
||||
* Make sure you have read the CONTRIBUTING.md file.
|
||||
* Make sure any relevant documentation and tests have been added/updated.
|
||||
* Do not submit Draft pull requests unless previously asked/agreed.
|
||||
@@ -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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
zizmor:
|
||||
name: Zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
@@ -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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
|
||||
@@ -74,6 +74,6 @@ unless System.get_env("DRYRUN") do
|
||||
"api-username" => "Elixir"
|
||||
}
|
||||
|
||||
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
|
||||
end
|
||||
|
||||
@@ -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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
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
|
||||
@@ -89,9 +88,9 @@ runs:
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0
|
||||
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
|
||||
with:
|
||||
image: ghcr.io/oss-review-toolkit/ort:92.2.0
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
|
||||
run: >-
|
||||
labels,
|
||||
cache-dependencies,
|
||||
@@ -107,6 +106,4 @@ runs:
|
||||
ort-cli-report-args: >-
|
||||
-O CycloneDX=output.file.formats=json,xml
|
||||
-O SpdxDocument=outputFileFormats=JSON,YAML
|
||||
ort-cli-scan-args: >-
|
||||
--scanners Provenant
|
||||
sw-version: "${{ inputs.version }}"
|
||||
|
||||
@@ -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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- 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@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
||||
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@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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,16 @@ 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 }}
|
||||
|
||||
OTP_GENERIC_VERSION: "25"
|
||||
steps:
|
||||
- name: "Check if variables are set up"
|
||||
if: "${{ ! vars.HEX_AWS_REGION }}"
|
||||
@@ -333,7 +317,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 +328,10 @@ jobs:
|
||||
|
||||
- name: Upload Precompiled to S3
|
||||
run: |
|
||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
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}" \
|
||||
@@ -355,17 +339,17 @@ jobs:
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
|
||||
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${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,9 +370,7 @@ jobs:
|
||||
- name: Update builds txt
|
||||
run: |
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name="$GITHUB_REF_NAME"
|
||||
|
||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
|
||||
touch builds.txt
|
||||
@@ -400,7 +382,7 @@ jobs:
|
||||
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
|
||||
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
|
||||
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
|
||||
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
|
||||
sed -i "/^${ref_name} /d" builds.txt
|
||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
fi
|
||||
@@ -439,7 +421,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,63 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
{
|
||||
"globs": [
|
||||
"**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".git/**",
|
||||
".github/**"
|
||||
],
|
||||
"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
|
||||
}
|
||||
@@ -37,11 +37,6 @@ curations:
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".github/pull_request_template.md"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to GitHub pull request template"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: ".gitignore"
|
||||
|
||||
+391
-77
@@ -4,108 +4,422 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.21
|
||||
# Changelog for Elixir v1.19
|
||||
|
||||
## v1.21.0-dev
|
||||
## Type system improvements
|
||||
|
||||
### Type checking of protocol dispatch and implementations
|
||||
|
||||
This release also adds type checking when dispatching and implementing protocols.
|
||||
|
||||
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
|
||||
|
||||
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def my_code(first..last//step = range) do
|
||||
"hello #{range}"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
the above emits the following warnings:
|
||||
|
||||
```
|
||||
warning: incompatible value given to string interpolation:
|
||||
|
||||
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:
|
||||
|
||||
binary()
|
||||
|
||||
but function has type:
|
||||
|
||||
(dynamic(map()) -> :map)
|
||||
|
||||
typing violation found at:
|
||||
│
|
||||
6 │ fun.("hello")
|
||||
│ ~
|
||||
│
|
||||
└─ 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/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
|
||||
|
||||
## Faster compile times in large projects
|
||||
|
||||
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
|
||||
|
||||
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.
|
||||
|
||||
### Code loading bottlenecks
|
||||
|
||||
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
|
||||
|
||||
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
|
||||
|
||||
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
|
||||
|
||||
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 module is not visible by 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_loaded!(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 in the past, and as a result of these changes, compilating 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, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
|
||||
|
||||
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
|
||||
|
||||
## Improved pretty printing algorithm
|
||||
|
||||
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
[1, 2, ...],
|
||||
[1, ...],
|
||||
[...],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on 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 by defining a `__escape__/1` callback that must return AST.
|
||||
|
||||
On the other, 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 and sponsored by the Erlang Ecosystem Foundation.
|
||||
|
||||
## v1.19.0-rc.2 (2025-10-07)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Support splitting middle expressions across EEx clauses
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
|
||||
* [Code] Add support for the `:erlc_options` compiler option
|
||||
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
|
||||
* [Kernel] Improve performance of type constructors and complex intersections
|
||||
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
|
||||
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
|
||||
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
|
||||
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
|
||||
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
|
||||
* [Path] Add `Path.safe_join/2`
|
||||
* [Registry] Optimize exact key matching in lookups
|
||||
* [String] Optimize `String.bag_distance/2`
|
||||
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
|
||||
* [URI] Optimize percent-decoding and `URI.to_string/1`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Add `trace/3` helper
|
||||
* [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
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y`
|
||||
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3`
|
||||
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision
|
||||
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats
|
||||
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments
|
||||
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
|
||||
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
|
||||
* [Date.Range] Fix slicing date ranges with stepped ranges
|
||||
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
|
||||
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
|
||||
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
|
||||
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
|
||||
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
|
||||
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
|
||||
* [Kernel] Fix inferred types for functions with non-returning clauses
|
||||
* [Kernel] Fix map field type inference in the presence of empty map types
|
||||
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
|
||||
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
|
||||
* [Kernel] Improve the error message for non-atom struct keys
|
||||
* [Kernel] Raise when `|` is used in guards
|
||||
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
|
||||
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
|
||||
* [Macro] Properly escape C1 control characters and Unicode noncharacters
|
||||
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
|
||||
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
|
||||
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
|
||||
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
|
||||
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
|
||||
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
|
||||
* [Kernel] Address issue with type checking not completing on protocol consolidation
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
|
||||
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys
|
||||
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
|
||||
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file
|
||||
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
|
||||
* [ExUnit] Do not crash on empty test unit groups
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Prevent synchronization lock files from being overwritten with empty contents
|
||||
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
|
||||
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
|
||||
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
|
||||
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
|
||||
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
|
||||
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
|
||||
* [mix new] Avoid trailing whitespace in generated files
|
||||
* [mix help] Add `mix help app:APP`
|
||||
* [mix test] Fix module preloading in `mix test --slowest-modules=N`
|
||||
|
||||
## v1.19.0-rc.1 (2025-10-05)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [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
|
||||
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
|
||||
* [OptionParser] Support the `:regex` type
|
||||
* [OptionParser] Enhance parsing error to display available options
|
||||
* [String] Update to Unicode 17.0.0
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Set a process label for each test
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Accept any enumerable in `Logger.metadata/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Add options to mix format to allow excluding of files
|
||||
* [mix test] Add `--name-pattern` option to `mix test`
|
||||
* [Mix.install/2] Support the `:compilers` option
|
||||
|
||||
### 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
|
||||
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
|
||||
* [Inspect] Inspect ill-formed structs as maps
|
||||
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Fix order in ExUnit results when listing pinned variables
|
||||
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Abort pipelines when there is an error in any step along the way
|
||||
|
||||
#### Mix
|
||||
|
||||
* [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 test] Prevent `mix test` from overriding `:failures_manifest_path` option
|
||||
|
||||
### 3. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
|
||||
* [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
|
||||
|
||||
### 4. Soft deprecations
|
||||
## v1.19.0-rc.0 (2025-06-09)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1`
|
||||
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
|
||||
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
|
||||
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
|
||||
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
|
||||
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
|
||||
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
|
||||
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
|
||||
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
|
||||
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
|
||||
* [Code.Fragment] Add `Code.Fragment.lines/1`
|
||||
* [Enum] Provide more information on `Enum.OutOfBoundsError`
|
||||
* [Inspect] Allow `optional: :all` when deriving Inspect
|
||||
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
|
||||
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
|
||||
* [Kernel] Allow controlling which applications are used during inference
|
||||
* [Kernel] Support `min/2` and `max/2` as guards
|
||||
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
|
||||
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
|
||||
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
|
||||
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
|
||||
* [Protocol] Type checking of protocols dispatch and implementations
|
||||
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
|
||||
* [String] Add `String.count/2` to count occurrences of a pattern
|
||||
|
||||
## v1.20
|
||||
#### ExUnit
|
||||
|
||||
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md).
|
||||
* [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
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
|
||||
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
|
||||
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
|
||||
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
|
||||
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
|
||||
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
|
||||
* [mix xref graph] Add support for `--format json`
|
||||
* [mix xref graph] Emit a warning if `--source` is part of a cycle
|
||||
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
|
||||
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
|
||||
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
|
||||
* [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
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix cmd] Preserve argument quoting in subcommands
|
||||
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
|
||||
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
|
||||
* [mix test] Preserve files with no longer filter on `mix test`
|
||||
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
|
||||
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
|
||||
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
|
||||
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}`, 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] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
|
||||
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
|
||||
|
||||
## v1.18
|
||||
|
||||
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
|
||||
|
||||
+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.
|
||||
|
||||
|
||||
+67
-114
@@ -55,7 +55,7 @@ To recompile all (including Erlang modules):
|
||||
make compile
|
||||
```
|
||||
|
||||
After your changes are done, run `make format` to guarantee
|
||||
After your changes are done, please remember to run `make format` to guarantee
|
||||
all files are properly formatted, then run the full suite with
|
||||
`make test`.
|
||||
|
||||
@@ -71,101 +71,80 @@ updating an existing checkout, run `make clean compile`. You can check
|
||||
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
|
||||
More tasks can be found by reading the [Makefile](Makefile).
|
||||
|
||||
## Sending a pull request
|
||||
|
||||
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
|
||||
and must include tests and other relevant proof of work:
|
||||
|
||||
* **Bug Fixes:** If you are fixing a bug, include a test that *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.
|
||||
We encourage contributors to write tests that capture both existing and newly
|
||||
introduced behavior, especially for bug fixes and major changes:
|
||||
|
||||
* **Bug Fixes:** If you are fixing a bug, please try to include a test that
|
||||
*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 assocaited tests. Aim to
|
||||
have the best code coverage possible.
|
||||
major changes to existing functionality, please add tests that cover the
|
||||
major parts of that functionality. Aim to have the best code coverage possible.
|
||||
|
||||
* **Performance improvements:** For performance improvements, please include the
|
||||
benchmark script, with inputs and results, in the pull request description.
|
||||
We recommend using [benchee](https://github.com/bencheeorg/benchee).
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
[send a pull request](https://help.github.com/articles/using-pull-requests/).
|
||||
We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
|
||||
Here are some pull requests we have received in the past you can use as reference:
|
||||
|
||||
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992)
|
||||
|
||||
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
|
||||
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
* [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
|
||||
|
||||
Once a pull request is sent, the Elixir team will review your changes.
|
||||
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.
|
||||
We outline our process below to clarify the roles of everyone involved.
|
||||
|
||||
All pull requests must be approved by two committers before being merged into
|
||||
the repository. If changes are necessary, the team will leave appropriate
|
||||
comments requesting changes to the code. Unfortunately, we cannot guarantee a
|
||||
pull request will be merged, even when modifications are requested, as the Elixir
|
||||
team will re-evaluate the contribution as it changes.
|
||||
|
||||
Committers may also push style changes directly to your branch. If you would
|
||||
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
|
||||
feature when submitting your pull request.
|
||||
|
||||
The Elixir team may optionally assign someone to review a pull request.
|
||||
If someone is assigned, they must explicitly approve the code before
|
||||
another team member can merge it.
|
||||
|
||||
When the review finishes, your pull request will be squashed and merged
|
||||
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 cloned and compiled 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://elixir.hexdocs.pm/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
|
||||
@@ -192,53 +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,
|
||||
security fixes, or correctness changes to the compiler or type system, 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).
|
||||
|
||||
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := main/
|
||||
# CANONICAL := main/
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
@@ -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)
|
||||
@@ -189,7 +187,7 @@ clean_elixir:
|
||||
#==> Documentation tasks
|
||||
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
+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*
|
||||
@@ -7,14 +7,14 @@
|
||||
<h1>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
|
||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200">
|
||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
|
||||
</picture>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
|
||||
[](https://www.bestpractices.dev/projects/10187)
|
||||
|
||||
Elixir is a programming language designed for building scalable
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
and maintainable applications.
|
||||
|
||||
For more about Elixir, installation and documentation,
|
||||
@@ -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
-4
@@ -12,16 +12,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.21 | Development
|
||||
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.21.0-dev
|
||||
ELIXIR_VERSION=1.19.0-rc.2
|
||||
|
||||
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.21.0-dev
|
||||
set ELIXIR_VERSION=1.19.0-rc.2
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+3
-7
@@ -236,9 +236,7 @@ defmodule EEx do
|
||||
"""
|
||||
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
|
||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
|
||||
|
||||
case tokenize(source, tokenize_opts) do
|
||||
case tokenize(source, options) do
|
||||
{:ok, tokens} ->
|
||||
EEx.Compiler.compile(tokens, source, options)
|
||||
|
||||
@@ -293,7 +291,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), Code.binding(), [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)
|
||||
@@ -315,7 +313,7 @@ defmodule EEx do
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t()
|
||||
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
@@ -344,7 +342,6 @@ defmodule EEx do
|
||||
|
||||
It returns `{:ok, [token]}` where a token is one of:
|
||||
|
||||
* `{:comment, content, %{column: column, line: line}}`
|
||||
* `{:text, content, %{column: column, line: line}}`
|
||||
* `{:expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:start_expr, marker, content, %{column: column, line: line}}`
|
||||
@@ -365,7 +362,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
|
||||
|
||||
+32
-74
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
|
||||
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr, extra_meta} =
|
||||
{key, expr} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, _column, _warnings, rev_tokens, []} ->
|
||||
# We ignore warnings because the code will be tokenized
|
||||
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
|
||||
token_key(rev_tokens, expr)
|
||||
|
||||
{:error, _, _, _, _} ->
|
||||
{:expr, expr, %{}}
|
||||
{:expr, expr}
|
||||
end
|
||||
|
||||
marker =
|
||||
@@ -96,14 +96,14 @@ 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
|
||||
end
|
||||
|
||||
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1))
|
||||
token = {key, marker, expr, %{line: line, column: column}}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,27 +127,6 @@ defmodule EEx.Compiler do
|
||||
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
||||
end
|
||||
|
||||
# Merge middle expressions separated only by whitespace so the whitespace is
|
||||
# part of the Elixir expression, not a separate EEx body.
|
||||
defp merge_token(
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
|
||||
) do
|
||||
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
|
||||
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
|
||||
else
|
||||
[
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
{:text, text, text_meta},
|
||||
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_token(token, acc) do
|
||||
[token | acc]
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
||||
@@ -198,37 +177,35 @@ defmodule EEx.Compiler do
|
||||
defp token_key(rev_tokens, expr) do
|
||||
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr, %{}}
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr), %{}}
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
|
||||
{_, [{:block_identifier, _, identifier} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}}
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr, %{}}
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:stab_op, _, _} | rev_tokens]} ->
|
||||
if fn_before_end?(rev_tokens) do
|
||||
{:start_expr, expr, %{}}
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
else
|
||||
{:middle_expr, expr, %{}}
|
||||
{:middle_expr, expr}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr, %{}}
|
||||
_ -> {:expr, expr, %{}}
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp fn_before_end?([{:fn, _} | _]), do: true
|
||||
defp fn_before_end?([{:end, _} | _]), do: false
|
||||
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
|
||||
defp fn_before_end?([]), do: false
|
||||
|
||||
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
|
||||
defp drop_eol(rest), do: rest
|
||||
|
||||
@@ -326,7 +303,7 @@ defmodule EEx.Compiler do
|
||||
file: file,
|
||||
source: source,
|
||||
line: line,
|
||||
quoted: %{},
|
||||
quoted: [],
|
||||
parser_options: [indentation: indentation] ++ parser_options,
|
||||
indentation: indentation
|
||||
}
|
||||
@@ -370,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
|
||||
|
||||
@@ -389,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
|
||||
|
||||
@@ -433,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
|
||||
@@ -449,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
|
||||
|
||||
@@ -466,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
|
||||
@@ -499,16 +476,11 @@ defmodule EEx.Compiler do
|
||||
Enum.all?(chars, &(&1 in @all_spaces))
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(%{block_identifier: identifier}) do
|
||||
identifier in [:else, :rescue, :catch]
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(_meta), do: false
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
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
|
||||
@@ -541,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
|
||||
|
||||
@@ -195,7 +195,7 @@ defmodule EEx.Engine do
|
||||
def handle_expr(state, "=", ast) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
||||
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__)
|
||||
var = Macro.var(:"arg#{vars_count}", __MODULE__)
|
||||
|
||||
ast =
|
||||
quote do
|
||||
|
||||
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
|
||||
{:text, ~c"bar", %{column: 21, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
|
||||
{:text, ~c"baz", %{column: 34, line: 1}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
|
||||
{:eof, %{column: 46, line: 1}}
|
||||
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
|
||||
exprs = [
|
||||
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
|
||||
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
|
||||
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
|
||||
{:eof, %{column: 3, line: 7}}
|
||||
|
||||
@@ -262,37 +262,6 @@ defmodule EExTest do
|
||||
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
||||
end
|
||||
|
||||
test "with expression with else clause split across tags" do
|
||||
template = """
|
||||
<%= with {:ok, x} <- @res do %>
|
||||
<p><%= x %></p>
|
||||
<% else %>
|
||||
<% _ -> %>
|
||||
<p>bad</p>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
|
||||
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
end
|
||||
|
||||
test "empty clauses separated by whitespace" do
|
||||
template = """
|
||||
<%= case x do %>
|
||||
<% :foo -> %>
|
||||
<% :bar -> %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n \n", template, x: :foo)
|
||||
assert_eval("\n\n", template, x: :bar)
|
||||
end
|
||||
|
||||
test "with end of token" do
|
||||
assert_eval("foo bar %>", "foo bar %>")
|
||||
end
|
||||
@@ -553,7 +522,7 @@ defmodule EExTest do
|
||||
|
||||
assert message |> Exception.message() |> strip_ansi() =~ """
|
||||
│
|
||||
#{line + 2} │ true && @some[\s
|
||||
514 │ true && @some[\s
|
||||
│ │ └ missing closing delimiter (expected "]")
|
||||
│ └ unclosed delimiter
|
||||
"""
|
||||
@@ -606,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
|
||||
)
|
||||
|
||||
+39
-90
@@ -10,8 +10,8 @@ defmodule Access do
|
||||
keys of any type in a data structure via the `data[key]` syntax.
|
||||
|
||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||
of the box. Keyword lists support only atom keys, while keys for maps
|
||||
can be of any type. Both return `nil` if the key does not exist:
|
||||
of the box. Keywords supports only atoms keys, keys for maps can
|
||||
be of any type. Both return `nil` if the key does not exist:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
@@ -33,7 +33,7 @@ defmodule Access do
|
||||
iex> keywords[:c][:unknown]
|
||||
nil
|
||||
|
||||
This works because accessing anything on a `nil` value returns
|
||||
This works because accessing anything on a `nil` value, returns
|
||||
`nil` itself:
|
||||
|
||||
iex> nil[:a]
|
||||
@@ -226,8 +226,6 @@ defmodule Access do
|
||||
end
|
||||
end
|
||||
|
||||
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
|
||||
|
||||
@doc """
|
||||
Fetches the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
@@ -487,7 +485,7 @@ defmodule Access do
|
||||
## Accessors
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -516,56 +514,30 @@ defmodule Access do
|
||||
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
||||
{"john", %{user: %{}}}
|
||||
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
|
||||
{"john", [user: []]}
|
||||
An error is raised if the accessed structure is not a map or a struct:
|
||||
|
||||
An error is raised if the accessed structure is not a map, struct, or keyword list:
|
||||
|
||||
iex> get_in(123, [Access.key(:foo)])
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
iex> get_in([], [Access.key(:foo)])
|
||||
** (BadMapError) expected a map, got:
|
||||
...
|
||||
"""
|
||||
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||
def key(key, default \\ nil) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
:get, data, next ->
|
||||
next.(Map.get(data, key, default))
|
||||
|
||||
:get_and_update, %{} = data, next ->
|
||||
:get_and_update, data, next ->
|
||||
value = Map.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Map.put(data, key, update)}
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -574,19 +546,6 @@ defmodule Access do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
{"john", [user: []]}
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
|
||||
** (KeyError) key :unknown not found in:
|
||||
...
|
||||
|
||||
iex> map = %{user: %{name: "john"}}
|
||||
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
@@ -615,15 +574,13 @@ defmodule Access do
|
||||
`Access.key!/1` is useful when the key is not known in advance
|
||||
and must be accessed dynamically.
|
||||
|
||||
An error is raised if the accessed structure is not a map/struct/keyword list:
|
||||
An error is raised if the accessed structure is not a map/struct:
|
||||
|
||||
iex> get_in(123, [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
|
||||
iex> get_in([], [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||
def key!(key) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
@@ -637,19 +594,8 @@ defmodule Access do
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.fetch!(data, key))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.fetch!(data, key)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -917,7 +863,7 @@ defmodule Access do
|
||||
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
||||
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
||||
|
||||
When no match is found, an empty list is returned and the update function is never called:
|
||||
When no match is found, an empty list is returned and the update function is never called
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
||||
@@ -927,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)])
|
||||
@@ -934,7 +885,7 @@ defmodule Access do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list)
|
||||
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
||||
def filter(func) when is_function(func) do
|
||||
fn op, data, next -> filter(op, data, func, next) end
|
||||
end
|
||||
@@ -1035,12 +986,12 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
||||
%Range{first: first, last: last, step: step} = normalize_range(range, data)
|
||||
range = normalize_range(range, data)
|
||||
|
||||
if first > last do
|
||||
if range.first > range.last do
|
||||
{[], data}
|
||||
else
|
||||
get_and_update_slice(data, first, last, step, next, [], [], 0)
|
||||
get_and_update_slice(data, range, next, [], [], 0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1145,23 +1096,16 @@ defmodule Access do
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index)
|
||||
when index > last do
|
||||
{:lists.reverse(gets), :lists.reverse(updates, rest)}
|
||||
end
|
||||
|
||||
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
|
||||
if index >= first and rem(index - first, step) == 0 do
|
||||
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||
if index in range do
|
||||
case next.(head) do
|
||||
:pop ->
|
||||
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1)
|
||||
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
|
||||
|
||||
{get, update} ->
|
||||
get_and_update_slice(
|
||||
rest,
|
||||
first,
|
||||
last,
|
||||
step,
|
||||
range,
|
||||
next,
|
||||
[update | updates],
|
||||
[get | gets],
|
||||
@@ -1169,11 +1113,11 @@ defmodule Access do
|
||||
)
|
||||
end
|
||||
else
|
||||
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1)
|
||||
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do
|
||||
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
|
||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@@ -1200,7 +1144,7 @@ defmodule Access do
|
||||
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
|
||||
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
|
||||
|
||||
When no match is found, nil is returned and the update function is never called:
|
||||
When no match is found, nil is returned and the update function is never called
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
|
||||
@@ -1210,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)])
|
||||
|
||||
@@ -327,7 +327,7 @@ defmodule Agent do
|
||||
passing the agent state. The result of the function invocation is
|
||||
returned from this function.
|
||||
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
@@ -366,7 +366,7 @@ defmodule Agent do
|
||||
elements, the first being the value to return (that is, the "get" value)
|
||||
and the second one being the new state of the agent.
|
||||
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
@@ -407,7 +407,7 @@ defmodule Agent do
|
||||
|
||||
This function always returns `:ok`.
|
||||
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -996,7 +991,7 @@ defmodule Application do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the directory for `app`.
|
||||
Gets the directory for app.
|
||||
|
||||
This information is returned based on the code path. Here is an
|
||||
example:
|
||||
@@ -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
|
||||
|
||||
@@ -1077,7 +1071,7 @@ defmodule Application do
|
||||
@doc """
|
||||
Formats the error reason returned by `start/2`,
|
||||
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
|
||||
and returns a string.
|
||||
returns a string.
|
||||
"""
|
||||
@spec format_error(any) :: String.t()
|
||||
def format_error(reason) do
|
||||
|
||||
+70
-294
@@ -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.
|
||||
|
||||
@@ -436,7 +187,7 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do
|
||||
name = String.to_unsafe_atom("encode16#{base}")
|
||||
name = :"encode16#{base}"
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -536,7 +287,7 @@ defmodule Base do
|
||||
* `:lower` - only allows lower case characters
|
||||
* `:mixed` - allows mixed case characters
|
||||
|
||||
An `ArgumentError` exception is raised if the string has an odd length or
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
@@ -617,24 +368,28 @@ defmodule Base do
|
||||
upper = Enum.with_index(b16_alphabet)
|
||||
|
||||
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
|
||||
decode_name = String.to_unsafe_atom("decode16#{base}!")
|
||||
validate_name = String.to_unsafe_atom("validate16#{base}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char16#{base}?")
|
||||
valid_word_name = String.to_unsafe_atom("valid_word16#{base}?")
|
||||
decode_name = :"decode16#{base}!"
|
||||
validate_name = :"validate16#{base}?"
|
||||
valid_char_name = :"valid_char16#{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)
|
||||
@@ -735,7 +490,7 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
|
||||
name = String.to_unsafe_atom("encode64#{base}")
|
||||
name = :"encode64#{base}"
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -1001,24 +756,28 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
|
||||
decode_name = String.to_unsafe_atom("decode64#{base}!")
|
||||
decode_name = :"decode64#{base}!"
|
||||
|
||||
validate_name = String.to_unsafe_atom("validate64#{base}?")
|
||||
validate_main_name = String.to_unsafe_atom("validate_main64#{validate_name}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char64#{base}?")
|
||||
valid_word_name = String.to_unsafe_atom("valid_word64#{base}?")
|
||||
validate_name = :"validate64#{base}?"
|
||||
validate_main_name = :"validate_main64#{validate_name}?"
|
||||
valid_char_name = :"valid_char64#{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)
|
||||
|
||||
@@ -1314,7 +1080,7 @@ defmodule Base do
|
||||
hexupper: b32hex_alphabet,
|
||||
hexlower: to_lower_enc.(b32hex_alphabet)
|
||||
] do
|
||||
name = String.to_unsafe_atom("encode32#{base}")
|
||||
name = :"encode32#{base}"
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -1388,7 +1154,7 @@ defmodule Base do
|
||||
|
||||
The values for `:case` can be:
|
||||
|
||||
* `:upper` - only allows upper case characters (default)
|
||||
* `:upper` - only allows upper case characters (default)
|
||||
* `:lower` - only allows lower case characters
|
||||
* `:mixed` - allows mixed case characters
|
||||
|
||||
@@ -1653,24 +1419,27 @@ defmodule Base do
|
||||
hexlower: to_lower_dec.(hexupper),
|
||||
hexmixed: to_mixed_dec.(hexupper)
|
||||
] do
|
||||
decode_name = String.to_unsafe_atom("decode32#{base}!")
|
||||
validate_name = String.to_unsafe_atom("validate32#{base}?")
|
||||
validate_main_name = String.to_unsafe_atom("validate_main32#{validate_name}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char32#{base}?")
|
||||
decode_name = :"decode32#{base}!"
|
||||
validate_name = :"validate32#{base}?"
|
||||
validate_main_name = :"validate_main32#{validate_name}?"
|
||||
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 = String.to_unsafe_atom("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)
|
||||
|
||||
|
||||
+29
-56
@@ -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.
|
||||
@@ -106,7 +104,6 @@ defmodule Calendar do
|
||||
@typedoc "Any map or struct that contains the time fields."
|
||||
@type time :: %{
|
||||
optional(any) => any,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
@@ -296,7 +293,7 @@ defmodule Calendar do
|
||||
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Defines the rollover moment for the calendar.
|
||||
Define the rollover moment for the calendar.
|
||||
|
||||
This is the moment, in your calendar, when the current day ends
|
||||
and the next day starts.
|
||||
@@ -382,13 +379,13 @@ defmodule Calendar do
|
||||
@callback iso_days_to_end_of_day(iso_days) :: iso_days
|
||||
|
||||
@doc """
|
||||
Shifts date by the given duration according to its calendar.
|
||||
Shifts date by given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
|
||||
|
||||
@doc """
|
||||
Shifts naive datetime by the given duration according to its calendar.
|
||||
Shifts naive datetime by given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_naive_datetime(
|
||||
@@ -403,7 +400,7 @@ defmodule Calendar do
|
||||
) :: {year, month, day, hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Shifts time by the given duration according to its calendar.
|
||||
Shifts time by given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
|
||||
@@ -532,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
|
||||
@@ -563,11 +559,11 @@ defmodule Calendar do
|
||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||
q | Quarter | 1, 2, 3, 4
|
||||
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
|
||||
S | Second | 00, 59
|
||||
S | Second | 00, 59, 60
|
||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||
x | Preferred date (without time) representation | 2018-10-17
|
||||
X | Preferred time (without date) representation | 12:34:56
|
||||
y | Year as 2-digits | -01, 01, 86, 18
|
||||
y | Year as 2-digits | 01, 01, 86, 18
|
||||
Y | Year | -0001, 0001, 1986
|
||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||
@@ -624,7 +620,7 @@ defmodule Calendar do
|
||||
...>)
|
||||
"серпень"
|
||||
|
||||
Microsecond formatting:
|
||||
Microsecond formatting:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
|
||||
"19-08-26 13:52:06.0"
|
||||
@@ -671,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
|
||||
@@ -712,7 +704,7 @@ defmodule Calendar do
|
||||
|
||||
# Literally just %
|
||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc])
|
||||
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||
end
|
||||
|
||||
# Abbreviated name of day
|
||||
@@ -781,7 +773,7 @@ defmodule Calendar do
|
||||
|
||||
# Day of the month
|
||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -800,35 +792,31 @@ defmodule Calendar do
|
||||
|
||||
# Hour using a 24-hour clock
|
||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 12-hour clock
|
||||
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the year
|
||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Month
|
||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Minute
|
||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -856,23 +844,19 @@ defmodule Calendar do
|
||||
|
||||
# Quarter
|
||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Second
|
||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the week
|
||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -922,25 +906,20 @@ defmodule Calendar do
|
||||
|
||||
# Year as 2-digits
|
||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
if datetime.year < 0 do
|
||||
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
||||
else
|
||||
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
end
|
||||
|
||||
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year
|
||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
{sign, year} =
|
||||
if datetime.year < 0 do
|
||||
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
||||
{?-, -datetime.year}
|
||||
else
|
||||
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
{[], datetime.year}
|
||||
end
|
||||
|
||||
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -987,7 +966,7 @@ defmodule Calendar do
|
||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||
|
||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}"
|
||||
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -1006,19 +985,13 @@ defmodule Calendar do
|
||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||
end
|
||||
|
||||
defp pad_preferred(result, width, pad) do
|
||||
result
|
||||
|> IO.iodata_to_binary()
|
||||
|> pad_leading(width, pad)
|
||||
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||
pad_preferred([pad | result], width, pad)
|
||||
end
|
||||
|
||||
defp pad_preferred(result, _width, _pad), do: result
|
||||
|
||||
defp pad_leading(string, count, padding) do
|
||||
to_pad = count - String.length(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
|
||||
defp pad_leading_ascii(string, count, padding) do
|
||||
to_pad = count - byte_size(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
@@ -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)
|
||||
@@ -81,7 +81,7 @@ defmodule Date do
|
||||
|
||||
Ranges of dates can be increasing (`first <= last`) and are
|
||||
always inclusive. For a decreasing range, use `range/3` with
|
||||
a step of -1 as third argument.
|
||||
a step of -1 as first argument.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -160,7 +160,7 @@ defmodule Date do
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"both dates must have matching calendar and the step must be a " <>
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}"
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
|
||||
end
|
||||
|
||||
defp range(first, first_days, last, last_days, calendar, step) do
|
||||
@@ -422,7 +422,7 @@ defmodule Date do
|
||||
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
|
||||
date
|
||||
|> convert!(Calendar.ISO)
|
||||
|> to_iso8601(format)
|
||||
|> to_iso8601()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -556,18 +556,14 @@ defmodule Date do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
|
||||
def compare(
|
||||
%{year: year1, month: month1, day: day1, calendar: calendar},
|
||||
%{year: year2, month: month2, day: day2, calendar: calendar}
|
||||
) do
|
||||
cond do
|
||||
year1 > year2 -> :gt
|
||||
year1 < year2 -> :lt
|
||||
month1 > month2 -> :gt
|
||||
month1 < month2 -> :lt
|
||||
day1 > day2 -> :gt
|
||||
day1 < day2 -> :lt
|
||||
true -> :eq
|
||||
def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
|
||||
%{year: year1, month: month1, day: day1} = date1
|
||||
%{year: year2, month: month2, day: day2} = date2
|
||||
|
||||
case {{year1, month1, day1}, {year2, month2, day2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1051,7 +1047,7 @@ defmodule Date do
|
||||
@doc """
|
||||
Calculates the quarter of the year of a given `date`.
|
||||
|
||||
Returns the quarter of the year as an integer. For the ISO 8601
|
||||
Returns the day of the year as an integer. For the ISO 8601
|
||||
calendar (the default), it is an integer from 1 to 4.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -37,19 +37,21 @@ defmodule Date.Range do
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
},
|
||||
} = range,
|
||||
%Date{calendar: calendar} = date
|
||||
) do
|
||||
{days, _} = Date.to_iso_days(date)
|
||||
|
||||
in_range? =
|
||||
if step > 0 do
|
||||
first_days <= days and days <= last_days and rem(days - first_days, step) == 0
|
||||
else
|
||||
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
|
||||
end
|
||||
cond do
|
||||
empty?(range) ->
|
||||
{:ok, false}
|
||||
|
||||
{:ok, in_range?}
|
||||
first_days <= last_days ->
|
||||
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
|
||||
|
||||
true ->
|
||||
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
|
||||
end
|
||||
end
|
||||
|
||||
def member?(%Date.Range{step: _}, _) do
|
||||
@@ -57,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
|
||||
@@ -86,7 +79,7 @@ defmodule Date.Range do
|
||||
step: step
|
||||
} = range
|
||||
) do
|
||||
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)}
|
||||
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
@@ -183,7 +176,7 @@ defmodule Date.Range do
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
}),
|
||||
do: div(last_days - first_days, step) + 1
|
||||
do: abs(div(last_days - first_days, step)) + 1
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp size(
|
||||
@@ -193,16 +186,43 @@ defmodule Date.Range do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
size(Map.put(date_range, :step, step))
|
||||
end
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step > 0 and first_days > last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step < 0 and first_days < last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{step: _}), do: false
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp empty?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
empty?(Map.put(date_range, :step, step))
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ defmodule DateTime do
|
||||
since v1.15.0.
|
||||
|
||||
The default unit if none gets passed is `:native`,
|
||||
which results in a default resolution of microseconds.
|
||||
which results on a default resolution of microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -720,9 +720,6 @@ defmodule DateTime do
|
||||
Other time zone databases can be passed as argument or set globally.
|
||||
See the "Time zone database" section in the module docs.
|
||||
|
||||
Shifting to the `"Etc/UTC"` time zone always succeeds without
|
||||
consulting the `time_zone_database`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||
@@ -756,28 +753,6 @@ defmodule DateTime do
|
||||
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
||||
end
|
||||
|
||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days_utc)
|
||||
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime}
|
||||
end
|
||||
|
||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
|
||||
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
|
||||
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
|
||||
@@ -1202,7 +1177,7 @@ defmodule DateTime do
|
||||
datetime
|
||||
|> to_iso_days()
|
||||
# Subtract total original offset in order to get UTC and add the new offset
|
||||
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second)
|
||||
|> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|
||||
|> calendar.naive_datetime_from_iso_days()
|
||||
end
|
||||
|
||||
@@ -1321,9 +1296,9 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of Gregorian seconds to a `DateTime` struct.
|
||||
Converts a number of gregorian seconds to a `DateTime` struct.
|
||||
|
||||
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use
|
||||
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
|
||||
`DateTime.shift_zone/3`.
|
||||
|
||||
## Examples
|
||||
@@ -1366,7 +1341,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds.
|
||||
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1601,15 +1576,15 @@ defmodule DateTime do
|
||||
def diff(datetime1, datetime2, unit \\ :second)
|
||||
|
||||
def diff(datetime1, datetime2, :day) do
|
||||
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000)
|
||||
diff(datetime1, datetime2, :second) |> div(86400)
|
||||
end
|
||||
|
||||
def diff(datetime1, datetime2, :hour) do
|
||||
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000)
|
||||
diff(datetime1, datetime2, :second) |> div(3600)
|
||||
end
|
||||
|
||||
def diff(datetime1, datetime2, :minute) do
|
||||
diff(datetime1, datetime2, :microsecond) |> div(60_000_000)
|
||||
diff(datetime1, datetime2, :second) |> div(60)
|
||||
end
|
||||
|
||||
def diff(
|
||||
@@ -1647,12 +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`.
|
||||
|
||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
||||
always succeeds without consulting the `time_zone_database`.
|
||||
`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
|
||||
@@ -1772,9 +1743,6 @@ defmodule DateTime do
|
||||
|
||||
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
|
||||
|
||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
||||
always succeeds without consulting the `time_zone_database`.
|
||||
|
||||
This operation is equivalent to shifting the datetime wall clock
|
||||
(in other words, the value as someone in that timezone would see
|
||||
on their watch), then applying the time zone offset to convert it
|
||||
@@ -1795,7 +1763,7 @@ defmodule DateTime do
|
||||
|
||||
Although the first example shows a difference of 2 hours when
|
||||
comparing the wall clocks of the given datetime with the returned one,
|
||||
due to the "spring forward" time jump, the actual elapsed time is
|
||||
due to the "spring forward" time jump, the actual ellapsed time is
|
||||
still exactly of 1 hour.
|
||||
|
||||
In case you don't want these changes to happen automatically or you
|
||||
@@ -1843,6 +1811,44 @@ defmodule DateTime do
|
||||
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
|
||||
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
|
||||
|
||||
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = datetime
|
||||
|
||||
{year, month, day, hour, minute, second, microsecond} =
|
||||
calendar.shift_naive_datetime(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
__duration__!(duration)
|
||||
)
|
||||
|
||||
%DateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
std_offset: 0,
|
||||
utc_offset: 0
|
||||
}
|
||||
end
|
||||
|
||||
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
|
||||
%{
|
||||
year: year,
|
||||
@@ -2053,12 +2059,11 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
defp apply_tz_offset(iso_days, offset) do
|
||||
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second)
|
||||
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
|
||||
end
|
||||
|
||||
defp from_map(%{} = datetime_map) do
|
||||
%DateTime{
|
||||
calendar: datetime_map.calendar,
|
||||
year: datetime_map.year,
|
||||
month: datetime_map.month,
|
||||
day: datetime_map.day,
|
||||
|
||||
@@ -72,7 +72,7 @@ defmodule Duration do
|
||||
|
||||
However, once again, it is important to remember that shifting a duration is not
|
||||
arithmetic, so you may want to use the functions in this module depending on what
|
||||
you want to achieve. Compare the results of both examples below:
|
||||
you to achieve. Compare the results of both examples below:
|
||||
|
||||
# Adding one month after the other
|
||||
iex> date = ~D[2016-01-31]
|
||||
@@ -129,16 +129,6 @@ defmodule Duration do
|
||||
second: 0,
|
||||
microsecond: {0, 0}
|
||||
|
||||
@typedoc """
|
||||
The microsecond component of a duration.
|
||||
|
||||
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
|
||||
durations may represent negative amounts of time. The precision is
|
||||
an integer from 0 to 6 holding the number of significant digits,
|
||||
as in the calendar types.
|
||||
"""
|
||||
@type microsecond :: {value :: integer, precision :: 0..6}
|
||||
|
||||
@typedoc """
|
||||
The duration struct type.
|
||||
"""
|
||||
@@ -150,7 +140,7 @@ defmodule Duration do
|
||||
hour: integer,
|
||||
minute: integer,
|
||||
second: integer,
|
||||
microsecond: microsecond()
|
||||
microsecond: Calendar.microsecond()
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
@@ -164,7 +154,7 @@ defmodule Duration do
|
||||
| {:hour, integer}
|
||||
| {:minute, integer}
|
||||
| {:second, integer}
|
||||
| {:microsecond, microsecond()}
|
||||
| {:microsecond, Calendar.microsecond()}
|
||||
|
||||
@typedoc """
|
||||
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
|
||||
@@ -242,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
|
||||
|
||||
@@ -253,26 +243,26 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec add(t, t) :: t
|
||||
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
def add(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
|
||||
%Duration{
|
||||
year: y1 + y2,
|
||||
month: mo1 + mo2,
|
||||
week: w1 + w2,
|
||||
day: day1 + day2,
|
||||
hour: h1 + h2,
|
||||
minute: mi1 + mi2,
|
||||
second: s1 + s2,
|
||||
microsecond: {ms1 + ms2, max(p1, p2)}
|
||||
year: d1.year + d2.year,
|
||||
month: d1.month + d2.month,
|
||||
week: d1.week + d2.week,
|
||||
day: d1.day + d2.day,
|
||||
hour: d1.hour + d2.hour,
|
||||
minute: d1.minute + d2.minute,
|
||||
second: d1.second + d2.second,
|
||||
microsecond: {m1 + m2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@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
|
||||
|
||||
@@ -283,19 +273,19 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec subtract(t, t) :: t
|
||||
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
def subtract(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
|
||||
%Duration{
|
||||
year: y1 - y2,
|
||||
month: mo1 - mo2,
|
||||
week: w1 - w2,
|
||||
day: day1 - day2,
|
||||
hour: h1 - h2,
|
||||
minute: mi1 - mi2,
|
||||
second: s1 - s2,
|
||||
microsecond: {ms1 - ms2, max(p1, p2)}
|
||||
year: d1.year - d2.year,
|
||||
month: d1.month - d2.month,
|
||||
week: d1.week - d2.week,
|
||||
day: d1.day - d2.day,
|
||||
hour: d1.hour - d2.hour,
|
||||
minute: d1.minute - d2.minute,
|
||||
second: d1.second - d2.second,
|
||||
microsecond: {m1 - m2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -312,16 +302,14 @@ defmodule Duration do
|
||||
"""
|
||||
@spec multiply(t, integer) :: t
|
||||
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: y * integer,
|
||||
month: mo * integer,
|
||||
week: w * integer,
|
||||
day: d * integer,
|
||||
hour: h * integer,
|
||||
minute: mi * integer,
|
||||
second: s * integer,
|
||||
year: duration.year * integer,
|
||||
month: duration.month * integer,
|
||||
week: duration.week * integer,
|
||||
day: duration.day * integer,
|
||||
hour: duration.hour * integer,
|
||||
minute: duration.minute * integer,
|
||||
second: duration.second * integer,
|
||||
microsecond: {ms * integer, p}
|
||||
}
|
||||
end
|
||||
@@ -339,16 +327,14 @@ defmodule Duration do
|
||||
"""
|
||||
@spec negate(t) :: t
|
||||
def negate(%Duration{microsecond: {ms, p}} = duration) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: -y,
|
||||
month: -mo,
|
||||
week: -w,
|
||||
day: -d,
|
||||
hour: -h,
|
||||
minute: -mi,
|
||||
second: -s,
|
||||
year: -duration.year,
|
||||
month: -duration.month,
|
||||
week: -duration.week,
|
||||
day: -duration.day,
|
||||
hour: -duration.hour,
|
||||
minute: -duration.minute,
|
||||
second: -duration.second,
|
||||
microsecond: {-ms, p}
|
||||
}
|
||||
end
|
||||
|
||||
+247
-213
@@ -33,9 +33,8 @@ defmodule Calendar.ISO do
|
||||
you to format datetimes however else you desire.
|
||||
|
||||
Elixir does not support reduced accuracy formats (for example, a date without
|
||||
the day component) nor decimal precisions in components other than seconds
|
||||
(such as `10:01,5`). Fractional seconds are supported and truncated to
|
||||
microsecond precision.
|
||||
the day component) nor decimal precisions in the lowest component (such as
|
||||
`10:01:25,5`).
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -82,11 +81,6 @@ defmodule Calendar.ISO do
|
||||
iex> Calendar.ISO.parse_time("23")
|
||||
{:error, :invalid_format}
|
||||
|
||||
A decimal fraction is accepted on seconds, but not on any other component:
|
||||
|
||||
iex> Calendar.ISO.parse_time("10:01:25,5")
|
||||
{:ok, {10, 1, 25, {500000, 1}}}
|
||||
|
||||
### Extensions
|
||||
|
||||
The parser and formatter adopt one ISO 8601 extension: extended year notation.
|
||||
@@ -188,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
|
||||
@@ -202,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
|
||||
[
|
||||
@@ -423,13 +403,16 @@ defmodule Calendar.ISO do
|
||||
@spec parse_date(String.t(), format) ::
|
||||
{:ok, {year, month, day}}
|
||||
| {:error, atom}
|
||||
def parse_date("-" <> string, format) when is_format(format),
|
||||
def parse_date(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_date_guarded(string, format)
|
||||
|
||||
defp parse_date_guarded("-" <> string, format),
|
||||
do: do_parse_date(string, -1, format)
|
||||
|
||||
def parse_date("+" <> string, format) when is_format(format),
|
||||
defp parse_date_guarded("+" <> string, format),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
def parse_date(string, format) when is_binary(string) and is_format(format),
|
||||
defp parse_date_guarded(string, format),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
|
||||
@@ -505,13 +488,16 @@ defmodule Calendar.ISO do
|
||||
@spec parse_naive_datetime(String.t(), format) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
def parse_naive_datetime("-" <> string, format) when is_format(format),
|
||||
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_naive_datetime_guarded(string, format)
|
||||
|
||||
defp parse_naive_datetime_guarded("-" <> string, format),
|
||||
do: do_parse_naive_datetime(string, -1, format)
|
||||
|
||||
def parse_naive_datetime("+" <> string, format) when is_format(format),
|
||||
defp parse_naive_datetime_guarded("+" <> string, format),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
defp parse_naive_datetime_guarded(string, format),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_naive_datetime(
|
||||
@@ -606,13 +592,16 @@ defmodule Calendar.ISO do
|
||||
@spec parse_utc_datetime(String.t(), format) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
|
||||
| {:error, atom}
|
||||
def parse_utc_datetime("-" <> string, format) when is_format(format),
|
||||
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_utc_datetime_guarded(string, format)
|
||||
|
||||
defp parse_utc_datetime_guarded("-" <> string, format),
|
||||
do: do_parse_utc_datetime(string, -1, format)
|
||||
|
||||
def parse_utc_datetime("+" <> string, format) when is_format(format),
|
||||
defp parse_utc_datetime_guarded("+" <> string, format),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
defp parse_utc_datetime_guarded(string, format),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_utc_datetime(
|
||||
@@ -663,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_time_unit_to_iso_days({0, day_fraction}, -offset, :second) 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
|
||||
|
||||
@@ -680,73 +669,75 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compatible unit pairs.
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
|
||||
|
||||
See `Duration.from_iso8601/1`.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
|
||||
def parse_duration("P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, 1, [], 0)
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
end
|
||||
|
||||
def parse_duration("+P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, 1, [], 0)
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
end
|
||||
|
||||
def parse_duration("-P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, -1, [], 0)
|
||||
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
|
||||
{:ok,
|
||||
Enum.map(fields, fn
|
||||
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
|
||||
{unit, value} -> {unit, -value}
|
||||
end)}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_duration(_) do
|
||||
{:error, :invalid_duration}
|
||||
end
|
||||
|
||||
defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc}
|
||||
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_date("T" <> string, sign, acc, _min_position)
|
||||
when byte_size(string) > 0 do
|
||||
parse_duration_time(string, sign, acc, 0)
|
||||
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
|
||||
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
|
||||
end
|
||||
|
||||
defp parse_duration_date(string, sign, acc, min_position) do
|
||||
with {integer, <<unit, rest::binary>>} <- Integer.parse(string),
|
||||
{key, next_min_position} <- find_date_unit(min_position, unit) do
|
||||
parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position)
|
||||
defp parse_duration_date(string, acc, allowed) do
|
||||
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
|
||||
{key, allowed} <- find_unit(allowed, next) do
|
||||
parse_duration_date(rest, [{key, integer} | acc], allowed)
|
||||
else
|
||||
_ -> {:error, :invalid_date_component}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc}
|
||||
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_time(string, sign, acc, min_position) do
|
||||
defp parse_duration_time(string, acc, allowed) do
|
||||
case Integer.parse(string) do
|
||||
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
|
||||
with {:second, _next_min_position} <- find_time_unit(min_position, ?S),
|
||||
{{ms, precision}, "S"} <- parse_microsecond(rest) do
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
case parse_microsecond(rest) do
|
||||
{{ms, precision}, "S"} ->
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
|
||||
|
||||
{:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc}
|
||||
else
|
||||
_ ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
{integer, <<unit, rest::binary>>} ->
|
||||
case find_time_unit(min_position, unit) do
|
||||
{key, next_min_position} ->
|
||||
parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position)
|
||||
|
||||
false ->
|
||||
{:error, :invalid_time_component}
|
||||
{integer, <<next, rest::binary>>} ->
|
||||
case find_unit(allowed, next) do
|
||||
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
|
||||
false -> {:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -754,17 +745,9 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
# The minimum position is the earliest unit still allowed, or one past the end.
|
||||
defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1}
|
||||
defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2}
|
||||
defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3}
|
||||
defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4}
|
||||
defp find_date_unit(_min_position, _unit), do: false
|
||||
|
||||
defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1}
|
||||
defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2}
|
||||
defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3}
|
||||
defp find_time_unit(_min_position, _unit), do: false
|
||||
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
|
||||
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
|
||||
defp find_unit([], _unit), do: false
|
||||
|
||||
@doc """
|
||||
Returns the `t:Calendar.iso_days/0` format of the specified date.
|
||||
@@ -801,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}}
|
||||
|
||||
"""
|
||||
@@ -895,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
|
||||
@@ -954,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.
|
||||
|
||||
@@ -1137,11 +1093,6 @@ defmodule Calendar.ISO do
|
||||
|
||||
It is an integer from 1 to 4.
|
||||
|
||||
In the ISO calendar, the quarter is determined solely by the month, so the `year`
|
||||
and `day` arguments are ignored. Combination of `year`, `month`, and `day` is not
|
||||
validated as a valid date, unlike in `day_of_year/3`. Use `valid_date?/3` when
|
||||
full date validation is required.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.quarter_of_year(2016, 1, 31)
|
||||
@@ -1182,9 +1133,9 @@ 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: {1 - year, 0}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
|
||||
|
||||
@doc """
|
||||
Calendar callback to compute the year and era from the
|
||||
@@ -1208,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 """
|
||||
@@ -1237,7 +1188,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
def day_of_era(year, month, day) when is_year_BCE(year) do
|
||||
day = @iso_epoch - date_to_iso_days(year, month, day)
|
||||
day = abs(date_to_iso_days(year, month, day) - @iso_epoch)
|
||||
{day, 0}
|
||||
end
|
||||
|
||||
@@ -1464,7 +1415,7 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Converts the given naive_datetime into a iodata.
|
||||
|
||||
See `naive_datetime_to_string/8` for more information.
|
||||
See `naive_datetime_to_iodata/8` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1589,7 +1540,7 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Converts the given datetime into a iodata.
|
||||
|
||||
See `datetime_to_string/12` for more information.
|
||||
See `datetime_to_iodata/12` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1693,7 +1644,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines if the time given is valid.
|
||||
Determines if the date given is valid according to the proleptic Gregorian calendar.
|
||||
|
||||
Leap seconds are not supported by the built-in Calendar.ISO.
|
||||
|
||||
@@ -1713,7 +1664,7 @@ defmodule Calendar.ISO do
|
||||
boolean
|
||||
def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond)
|
||||
when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and
|
||||
is_integer(ms_precision) do
|
||||
is_integer(ms_value) do
|
||||
is_hour(hour) and is_minute(minute) and is_second(second) and
|
||||
is_microsecond(ms_value, ms_precision)
|
||||
end
|
||||
@@ -1753,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}}
|
||||
|
||||
"""
|
||||
@@ -1773,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}}
|
||||
|
||||
"""
|
||||
@@ -1846,9 +1797,6 @@ defmodule Calendar.ISO do
|
||||
shift_options = shift_datetime_options(duration)
|
||||
|
||||
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
|
||||
{:microsecond, {0, _}}, naive_datetime ->
|
||||
naive_datetime
|
||||
|
||||
{_, 0}, naive_datetime ->
|
||||
naive_datetime
|
||||
|
||||
@@ -1878,9 +1826,6 @@ defmodule Calendar.ISO do
|
||||
shift_options = shift_time_options(duration)
|
||||
|
||||
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
|
||||
{:microsecond, {0, _}}, time ->
|
||||
time
|
||||
|
||||
{_, 0}, time ->
|
||||
time
|
||||
|
||||
@@ -1892,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()
|
||||
|
||||
@@ -1903,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
|
||||
@@ -1943,7 +1888,8 @@ 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
|
||||
add_time_unit_to_iso_days(iso_days, value, unit)
|
||||
ppd = System.convert_time_unit(86400, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, value, ppd)
|
||||
end
|
||||
|
||||
defp shift_time_unit_values({0, _}, {_, original_precision}) do
|
||||
@@ -1991,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
|
||||
@@ -2024,8 +1970,8 @@ defmodule Calendar.ISO do
|
||||
total = System.convert_time_unit(integer, unit, :microsecond)
|
||||
|
||||
if total in @unix_range_microseconds do
|
||||
{seconds, microseconds} = div_rem(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + seconds
|
||||
microseconds = Integer.mod(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}}
|
||||
@@ -2046,27 +1992,38 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_microsecond("." <> rest), do: parse_microsecond(rest, rest, 0)
|
||||
defp parse_microsecond("," <> rest), do: parse_microsecond(rest, rest, 0)
|
||||
defp parse_microsecond(rest), do: {{0, 0}, rest}
|
||||
defp parse_microsecond("." <> rest) do
|
||||
case parse_microsecond(rest, 0, []) do
|
||||
{[], 0, _} ->
|
||||
:error
|
||||
|
||||
# Digits past the sixth are consumed but do not contribute to the value.
|
||||
defp parse_microsecond(<<head, tail::binary>>, digits, 6) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, digits, 6)
|
||||
{microsecond, precision, rest} ->
|
||||
scale = scale_factor(precision)
|
||||
{{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_microsecond(<<head, tail::binary>>, digits, precision) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, digits, precision + 1)
|
||||
defp parse_microsecond("," <> rest) do
|
||||
parse_microsecond("." <> rest)
|
||||
end
|
||||
|
||||
defp parse_microsecond(_rest, _digits, 0), do: :error
|
||||
defp parse_microsecond(rest) do
|
||||
{{0, 0}, rest}
|
||||
end
|
||||
|
||||
defp parse_microsecond(rest, digits, precision) do
|
||||
scale = scale_factor(precision)
|
||||
microsecond = :erlang.binary_to_integer(:binary.part(digits, 0, precision)) * scale
|
||||
{{microsecond, precision}, rest}
|
||||
defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, 6, acc)
|
||||
|
||||
defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, precision + 1, [head | acc])
|
||||
|
||||
defp parse_microsecond(rest, precision, acc) do
|
||||
{:lists.reverse(acc), precision, rest}
|
||||
end
|
||||
|
||||
defp parse_offset(""), do: {nil, ""}
|
||||
defp parse_offset("Z"), do: {0, ""}
|
||||
defp parse_offset("-00:00"), do: :error
|
||||
|
||||
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
|
||||
do: parse_offset(1, h1, h2, m1, m2, rest)
|
||||
@@ -2089,8 +2046,7 @@ defmodule Calendar.ISO do
|
||||
true <- m1 in ?0..?5 and m2 in ?0..?9,
|
||||
hour = (h1 - ?0) * 10 + h2 - ?0,
|
||||
min = (m1 - ?0) * 10 + m2 - ?0,
|
||||
true <- hour < 24,
|
||||
true <- sign == 1 or hour != 0 or min != 0 do
|
||||
true <- hour < 24 do
|
||||
{(hour * 60 + min) * 60 * sign, rest}
|
||||
else
|
||||
_ -> :error
|
||||
@@ -2106,43 +2062,10 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def iso_days_to_unit(iso_days, :second) do
|
||||
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), @microseconds_per_second)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, :millisecond) do
|
||||
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), 1_000)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, :microsecond) do
|
||||
iso_days_to_microseconds(iso_days)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, unit) do
|
||||
System.convert_time_unit(iso_days_to_microseconds(iso_days), :microsecond, unit)
|
||||
end
|
||||
|
||||
defp iso_days_to_microseconds({days, {parts, ppd}}) do
|
||||
days * @parts_per_day + divide_by_parts_per_day(parts, ppd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_time_unit_to_iso_days(iso_days, add, :second) do
|
||||
add_day_fraction_to_iso_days(iso_days, add * @microseconds_per_second, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, :millisecond) do
|
||||
add_day_fraction_to_iso_days(iso_days, add * 1_000, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, :microsecond) do
|
||||
add_day_fraction_to_iso_days(iso_days, add, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, unit)
|
||||
when unit == :nanosecond or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(@seconds_per_day, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, add, ppd)
|
||||
def iso_days_to_unit({days, {parts, ppd}}, unit) do
|
||||
day_microseconds = days * @parts_per_day
|
||||
microseconds = divide_by_parts_per_day(parts, ppd)
|
||||
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -2170,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
|
||||
@@ -2189,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)
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ defmodule NaiveDateTime do
|
||||
Returns the "local time" for the machine the Elixir program is running on.
|
||||
|
||||
WARNING: This function can cause insidious bugs. It depends on the time zone
|
||||
configuration at run time. This can change and be set to a time zone that has
|
||||
configuration at run time. This can changed and be set to a time zone that has
|
||||
daylight saving jumps (spring forward or fall back).
|
||||
|
||||
This function can be used to display what the time is right now for the time
|
||||
@@ -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
|
||||
|
||||
@@ -541,15 +540,15 @@ defmodule NaiveDateTime do
|
||||
def diff(naive_datetime1, naive_datetime2, unit \\ :second)
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :day) do
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000)
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
|
||||
end
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :hour) do
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000)
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
|
||||
end
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :minute) do
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000)
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(60)
|
||||
end
|
||||
|
||||
def diff(
|
||||
@@ -570,11 +569,9 @@ defmodule NaiveDateTime do
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
diff_microsecond =
|
||||
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
|
||||
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
|
||||
|
||||
System.convert_time_unit(diff_microsecond, :microsecond, unit)
|
||||
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units1 - units2
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -674,7 +671,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t()
|
||||
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
|
||||
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
|
||||
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
|
||||
end
|
||||
@@ -717,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
|
||||
|
||||
@@ -745,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,
|
||||
@@ -1152,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,
|
||||
|
||||
@@ -62,7 +62,7 @@ defmodule Time do
|
||||
|
||||
You can pass a time unit to automatically truncate the resulting time.
|
||||
|
||||
The default unit if none gets passed is `:native` which results in a default resolution of microseconds.
|
||||
The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -146,9 +146,8 @@ defmodule Time do
|
||||
iex> Time.new(23, 59, 59, 1_000_000)
|
||||
{:error, :invalid_time}
|
||||
|
||||
Invalid precision:
|
||||
|
||||
iex> Time.new(23, 59, 59, {999_999, 10})
|
||||
# Invalid precision
|
||||
Time.new(23, 59, 59, {999_999, 10})
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@@ -467,12 +466,8 @@ defmodule Time do
|
||||
Calendar.microsecond(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_seconds_after_midnight(
|
||||
seconds,
|
||||
{microsecond, precision} \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
|
||||
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
when is_integer(seconds) do
|
||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||
|
||||
{hour, minute, second, {_, _}} =
|
||||
@@ -483,7 +478,7 @@ defmodule Time do
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
microsecond: microsecond
|
||||
}
|
||||
end
|
||||
|
||||
@@ -501,7 +496,7 @@ defmodule Time do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()}
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||
@@ -519,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.
|
||||
@@ -694,7 +688,7 @@ defmodule Time do
|
||||
@doc """
|
||||
Compares two time structs.
|
||||
|
||||
Returns `:gt` if the first time is later than the second
|
||||
Returns `:gt` if first time is later than the second
|
||||
and `:lt` for vice versa. If the two times are equal
|
||||
`:eq` is returned.
|
||||
|
||||
@@ -720,32 +714,14 @@ defmodule Time do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
|
||||
def compare(
|
||||
%{
|
||||
hour: hour1,
|
||||
minute: minute1,
|
||||
second: second1,
|
||||
microsecond: {microsecond1, _},
|
||||
calendar: calendar
|
||||
},
|
||||
%{
|
||||
hour: hour2,
|
||||
minute: minute2,
|
||||
second: second2,
|
||||
microsecond: {microsecond2, _},
|
||||
calendar: calendar
|
||||
}
|
||||
) do
|
||||
cond do
|
||||
hour1 > hour2 -> :gt
|
||||
hour1 < hour2 -> :lt
|
||||
minute1 > minute2 -> :gt
|
||||
minute1 < minute2 -> :lt
|
||||
second1 > second2 -> :gt
|
||||
second1 < second2 -> :lt
|
||||
microsecond1 > microsecond2 -> :gt
|
||||
microsecond1 < microsecond2 -> :lt
|
||||
true -> :eq
|
||||
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
|
||||
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
|
||||
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
|
||||
|
||||
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -924,11 +900,11 @@ defmodule Time do
|
||||
def diff(time1, time2, unit \\ :second)
|
||||
|
||||
def diff(time1, time2, :hour) do
|
||||
diff(time1, time2, :microsecond) |> div(3_600_000_000)
|
||||
diff(time1, time2, :second) |> div(3600)
|
||||
end
|
||||
|
||||
def diff(time1, time2, :minute) do
|
||||
diff(time1, time2, :microsecond) |> div(60_000_000)
|
||||
diff(time1, time2, :second) |> div(60)
|
||||
end
|
||||
|
||||
def diff(
|
||||
|
||||
+91
-162
@@ -50,7 +50,7 @@ defmodule Code do
|
||||
|
||||
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
|
||||
`ensure_loaded!/1`) to check if a module is loaded before using it and
|
||||
act accordingly.
|
||||
act.
|
||||
|
||||
## `ensure_compiled/1` and `ensure_compiled!/1`
|
||||
|
||||
@@ -258,7 +258,6 @@ defmodule Code do
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:force_do_end_blocks, boolean()}
|
||||
| {:migrate, boolean()}
|
||||
| {:migrate_atom_interpolations, boolean()}
|
||||
| {:migrate_bitstring_modifiers, boolean()}
|
||||
| {:migrate_call_parens_on_pipe, boolean()}
|
||||
| {:migrate_charlists_as_sigils, boolean()}
|
||||
@@ -273,7 +272,6 @@ defmodule Code do
|
||||
| {:escape, boolean()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:comments, [term()]}
|
||||
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
|
||||
|
||||
@typedoc """
|
||||
Options for parsing functions that convert strings to quoted expressions.
|
||||
@@ -287,26 +285,20 @@ defmodule Code do
|
||||
unescape: boolean(),
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
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,
|
||||
@@ -316,16 +308,11 @@ defmodule Code do
|
||||
:relative_paths
|
||||
]
|
||||
|
||||
@list_compiler_options [:tracers, :parser_options, :erlc_options]
|
||||
@list_compiler_options [:tracers, :parser_options]
|
||||
|
||||
@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.
|
||||
@@ -415,7 +402,7 @@ defmodule Code do
|
||||
operations.
|
||||
|
||||
"""
|
||||
@spec append_path(Path.t(), cache: boolean()) :: boolean()
|
||||
@spec append_path(Path.t(), cache: boolean()) :: true | false
|
||||
def append_path(path, opts \\ []) do
|
||||
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
|
||||
end
|
||||
@@ -554,7 +541,8 @@ defmodule Code do
|
||||
This is the list of directories the Erlang VM uses for finding
|
||||
module code. The list of files is managed per Erlang VM node.
|
||||
|
||||
All paths are expanded with `Path.expand/1` before being deleted.
|
||||
The path is expanded with `Path.expand/1` before being deleted. If the
|
||||
path does not exist, this function returns `false`.
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec delete_paths([Path.t()]) :: :ok
|
||||
@@ -580,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
|
||||
@@ -614,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
|
||||
@@ -626,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
|
||||
|
||||
@@ -768,12 +747,6 @@ defmodule Code do
|
||||
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
|
||||
to `true` by default. Defaults to `false`.
|
||||
|
||||
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
|
||||
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
|
||||
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
|
||||
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
|
||||
Defaults to the value of the `:migrate` option. This option changes the AST.
|
||||
|
||||
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
|
||||
removes unnecessary parentheses in known bitstring
|
||||
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
|
||||
@@ -1117,7 +1090,7 @@ defmodule Code do
|
||||
@doc since: "1.6.0"
|
||||
@spec format_string!(binary, [format_opt]) :: iodata
|
||||
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
|
||||
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
|
||||
line_length = Keyword.get(opts, :line_length, 98)
|
||||
|
||||
to_quoted_opts =
|
||||
[
|
||||
@@ -1156,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
|
||||
|
||||
@@ -1179,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
|
||||
|
||||
@@ -1220,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 """
|
||||
@@ -1236,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
|
||||
@@ -1298,9 +1258,9 @@ defmodule Code do
|
||||
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
|
||||
It must be a function that receives two arguments, the literal and its
|
||||
metadata, and it must return `{:ok, ast :: Macro.t}` or
|
||||
`{:error, reason :: binary}`. If you return anything other than the literal
|
||||
`{:error, reason :: binary}`. If you return anything than the literal
|
||||
itself as the `term`, then the AST is no longer valid. This option
|
||||
may still be useful for textual analysis of the source code.
|
||||
may still useful for textual analysis of the source code.
|
||||
|
||||
* `:static_atoms_encoder` - the static atom encoder function, see
|
||||
"The `:static_atoms_encoder` function" section below. Note this
|
||||
@@ -1326,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
|
||||
@@ -1365,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 """
|
||||
@@ -1394,15 +1361,12 @@ defmodule Code do
|
||||
while preserving information like comments and literals position.
|
||||
|
||||
Returns `{:ok, quoted_form, comments}` if it succeeds,
|
||||
`{:error, {location, error, token}}` otherwise, where `location`
|
||||
is keyword metadata containing the line and column of the error.
|
||||
`{:error, {line, error, token}}` otherwise.
|
||||
|
||||
Comments are maps with the following fields:
|
||||
|
||||
* `:line` - The line number of the source code
|
||||
|
||||
* `:column` - The column number of the source code
|
||||
|
||||
* `:text` - The full text of the comment, including the leading `#`
|
||||
|
||||
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
|
||||
@@ -1444,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
|
||||
@@ -1457,9 +1422,7 @@ defmodule Code do
|
||||
|
||||
Returns the AST and a list of comments if it succeeds, raises an exception
|
||||
otherwise. The exception is a `TokenMissingError` in case a token is missing
|
||||
(usually because the expression is incomplete), `MismatchedDelimiterError`
|
||||
(in case of mismatched opening and closing delimiters) and `SyntaxError`
|
||||
otherwise.
|
||||
(usually because the expression is incomplete), `SyntaxError` otherwise.
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@@ -1562,13 +1525,9 @@ defmodule Code do
|
||||
`string_to_quoted/2`, setting this option to `false` will prevent it from
|
||||
escaping the sequences twice. Defaults to `true`.
|
||||
|
||||
* `:syntax_colors` - a keyword list of colors the output is colorized.
|
||||
See `Inspect.Opts` for more information.
|
||||
|
||||
See `format_string!/2` for the full list of formatting options including
|
||||
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all
|
||||
migration options like `:migrate_charlists_as_sigils`. Note `:line_length`
|
||||
does not apply here.
|
||||
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
|
||||
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
|
||||
@@ -1650,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
|
||||
|
||||
@@ -1688,7 +1641,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores all given compilation options.
|
||||
|
||||
Changing the compilation options affects all processes
|
||||
Changing the compilation options affect all processes
|
||||
running in a given Erlang VM node. To store individual
|
||||
options and for a description of all options, see
|
||||
`put_compiler_option/2`.
|
||||
@@ -1752,11 +1705,14 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores a compilation option.
|
||||
|
||||
Changing the compilation options affects all processes running in a
|
||||
Changing the compilation options affect all processes running in a
|
||||
given Erlang VM node.
|
||||
|
||||
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`.
|
||||
@@ -1768,17 +1724,10 @@ defmodule Code do
|
||||
remove the `:debug_info` while deploying, tools like `mix release`
|
||||
already do such by default.
|
||||
|
||||
Other environments, such as `mix test`, automatically disable this
|
||||
Other environments, such as `mix test`, automatically disables this
|
||||
via the `:test_elixirc_options` project configuration, as there is
|
||||
typically no need to store debug chunks for test files.
|
||||
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
|
||||
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
|
||||
debug metadata for BEAM debuggers. Defaults to `[]`.
|
||||
|
||||
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
|
||||
when a protocol has already been consolidated and a new implementation is added.
|
||||
Defaults to `false`.
|
||||
@@ -1786,41 +1735,28 @@ defmodule Code do
|
||||
* `:ignore_module_conflict` - when `true`, does not warn when a module has
|
||||
already been defined. Defaults to `false`.
|
||||
|
||||
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules
|
||||
should be used during type inference. When `false`, it disables module-local
|
||||
* `:infer_signatures` (since v1.18.0) - a list of applications of which modules
|
||||
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
|
||||
signatures 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 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
|
||||
@@ -1831,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.
|
||||
|
||||
@@ -1876,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
|
||||
@@ -2150,7 +2079,7 @@ defmodule Code do
|
||||
If the module being checked is currently in a compiler deadlock,
|
||||
this function returns `{:error, :unavailable}`. Unavailable doesn't
|
||||
necessarily mean the module doesn't exist, just that it is not currently
|
||||
available, but it may (or may not) become available in the future.
|
||||
available, but it (or may not) become available in the future.
|
||||
|
||||
Therefore, if you can only continue if the module is available, use
|
||||
`ensure_compiled!/1` instead. In particular, do not do this:
|
||||
|
||||
@@ -195,7 +195,6 @@ defmodule Code.Formatter do
|
||||
file = Keyword.get(opts, :file, nil)
|
||||
sigils = Keyword.get(opts, :sigils, [])
|
||||
migrate = Keyword.get(opts, :migrate, false)
|
||||
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
|
||||
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
|
||||
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
|
||||
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
|
||||
@@ -224,7 +223,6 @@ defmodule Code.Formatter do
|
||||
comments: comments,
|
||||
sigils: sigils,
|
||||
file: file,
|
||||
migrate_atom_interpolations: migrate_atom_interpolations,
|
||||
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
|
||||
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
|
||||
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
|
||||
@@ -336,20 +334,14 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra(
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} =
|
||||
quoted,
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
|
||||
context,
|
||||
state
|
||||
) do
|
||||
cond do
|
||||
not interpolated?(entries) ->
|
||||
remote_to_algebra(quoted, context, state)
|
||||
|
||||
state.migrate_atom_interpolations ->
|
||||
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
|
||||
|
||||
true ->
|
||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||
if interpolated?(entries) do
|
||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||
else
|
||||
remote_to_algebra(quoted, context, state)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1465,7 +1457,7 @@ defmodule Code.Formatter do
|
||||
metadata = [
|
||||
file: state.file,
|
||||
line: meta[:line],
|
||||
sigil: String.to_unsafe_atom(name),
|
||||
sigil: String.to_atom(name),
|
||||
modifiers: modifiers,
|
||||
opening_delimiter: opening_delimiter
|
||||
]
|
||||
@@ -1704,7 +1696,7 @@ defmodule Code.Formatter do
|
||||
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
|
||||
end
|
||||
|
||||
defp integer_to_algebra(text, inspect_opts) do
|
||||
defp integer_to_algebra(text, inspect_otps) do
|
||||
case text do
|
||||
<<?0, ?x, rest::binary>> ->
|
||||
"0x" <> String.upcase(rest)
|
||||
@@ -1718,15 +1710,15 @@ defmodule Code.Formatter do
|
||||
decimal ->
|
||||
insert_underscores(decimal)
|
||||
end
|
||||
|> color_doc(:number, inspect_opts)
|
||||
|> color_doc(:number, inspect_otps)
|
||||
end
|
||||
|
||||
defp float_to_algebra(text, inspect_opts) do
|
||||
defp float_to_algebra(text, inspect_otps) do
|
||||
[int_part, decimal_part] = :binary.split(text, ".")
|
||||
decimal_part = String.downcase(decimal_part)
|
||||
|
||||
string = insert_underscores(int_part) <> "." <> decimal_part
|
||||
color_doc(string, :number, inspect_opts)
|
||||
color_doc(string, :number, inspect_otps)
|
||||
end
|
||||
|
||||
defp insert_underscores("-" <> digits) do
|
||||
|
||||
@@ -27,8 +27,7 @@ defmodule Code.Fragment do
|
||||
column: pos_integer(),
|
||||
columns: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
||||
preserve_sigils: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
trailing_fragment: String.t()
|
||||
]
|
||||
|
||||
@@ -36,7 +35,7 @@ defmodule Code.Fragment do
|
||||
Returns the list of lines in the given string, preserving their line endings.
|
||||
|
||||
Only the line endings recognized by the Elixir compiler are
|
||||
considered, namely `\r\n` and `\n`. If you would like to retrieve
|
||||
considered, namely `\r\n` and `\n`. If you would like the retrieve
|
||||
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
|
||||
|
||||
## Examples
|
||||
@@ -52,7 +51,6 @@ defmodule Code.Fragment do
|
||||
|
||||
"""
|
||||
@doc since: "1.19.0"
|
||||
@spec lines(String.t()) :: [String.t()]
|
||||
def lines(string) do
|
||||
lines(string, <<>>)
|
||||
end
|
||||
@@ -142,9 +140,6 @@ defmodule Code.Fragment do
|
||||
* `{:anonymous_call, inside_caller}` - the context is an anonymous
|
||||
call, such as `fun.(` and `@fun.(`.
|
||||
|
||||
* `{:capture_arg, charlist}` - the context is a capture argument,
|
||||
such as `&1`
|
||||
|
||||
* `{:module_attribute, charlist}` - the context is a module attribute,
|
||||
such as `@hello_wor`
|
||||
|
||||
@@ -162,8 +157,8 @@ defmodule Code.Fragment do
|
||||
* `:none` - no context possible
|
||||
|
||||
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
|
||||
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as
|
||||
`~>` and `~>>`, are returned as :operator contexts
|
||||
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
|
||||
`~>` and `~>>`
|
||||
|
||||
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
|
||||
`inside_struct` can either be a `charlist` in case of a static alias or an
|
||||
@@ -209,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}
|
||||
@@ -662,7 +656,7 @@ defmodule Code.Fragment do
|
||||
iex> Code.Fragment.surround_context("foo", {1, 1})
|
||||
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
|
||||
|
||||
## Differences from `cursor_context/2`
|
||||
## Differences to `cursor_context/2`
|
||||
|
||||
Because `surround_context/3` attempts to capture complex expressions,
|
||||
it has some differences to `cursor_context/2`:
|
||||
@@ -676,7 +670,7 @@ defmodule Code.Fragment do
|
||||
be a local or variable
|
||||
|
||||
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`)
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
|
||||
|
||||
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
|
||||
`{:struct, ~c""}` as context
|
||||
@@ -1240,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)
|
||||
@@ -1296,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 =
|
||||
@@ -1307,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 =
|
||||
@@ -1326,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
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
|
||||
with "-" <> rest <- Atom.to_string(atom),
|
||||
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
||||
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity}
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
@@ -150,20 +150,20 @@ defmodule Code.Identifier do
|
||||
<<acc::binary, char::utf8>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x80 do
|
||||
defp escape_char(char, acc) when char < 0x100 do
|
||||
<<a::4, b::4>> = <<char::8>>
|
||||
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x10000 do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x1000000 do
|
||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
||||
|
||||
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
?}>>
|
||||
end
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
defmodule Code.Normalizer do
|
||||
@moduledoc false
|
||||
|
||||
@do_end_keywords [:rescue, :catch, :else, :after]
|
||||
|
||||
defguard is_literal(x)
|
||||
when is_integer(x) or
|
||||
is_float(x) or
|
||||
@@ -70,7 +68,7 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Bit containers
|
||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_bitstring(quoted, state, state.escape)
|
||||
normalize_bitstring(quoted, state)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
@@ -91,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
|
||||
|
||||
@@ -114,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)
|
||||
@@ -184,8 +187,7 @@ defmodule Code.Normalizer do
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
string = normalize_bitstring(string, %{state | parent_meta: meta}, false)
|
||||
{sigil, meta, [string, modifiers]}
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
else
|
||||
_ ->
|
||||
normalize_call(quoted, state)
|
||||
@@ -267,7 +269,7 @@ defmodule Code.Normalizer do
|
||||
"Elixir." <> segments ->
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_unsafe_atom/1)
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
end
|
||||
|
||||
{:__aliases__, meta, segments}
|
||||
@@ -349,20 +351,18 @@ defmodule Code.Normalizer do
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
|
||||
Keyword.has_key?(meta, :do) and kw_blocks?(last) ->
|
||||
Keyword.has_key?(meta, :do) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], last) and kw_blocks?(last) ->
|
||||
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
|
||||
# Non normalized kw blocks
|
||||
line = state.parent_meta[:line] || meta[:line]
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
true ->
|
||||
# The formatter renders do-end blocks from the meta alone
|
||||
meta = Keyword.drop(meta, [:do, :end])
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||
|
||||
@@ -401,22 +401,11 @@ defmodule Code.Normalizer do
|
||||
defp block_keyword?([]), do: true
|
||||
defp block_keyword?(_), do: false
|
||||
|
||||
# Anything after the do block that is not a block keyword makes it a keyword list
|
||||
defp kw_blocks?([{:do, _} | rest] = kw) do
|
||||
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
|
||||
end
|
||||
|
||||
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
|
||||
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
|
||||
end
|
||||
|
||||
defp kw_blocks?(_), do: false
|
||||
|
||||
defp allow_keyword?(:when, 2), do: true
|
||||
defp allow_keyword?(:{}, _), 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
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
parts =
|
||||
@@ -435,21 +424,9 @@ defmodule Code.Normalizer do
|
||||
end)
|
||||
end
|
||||
|
||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
||||
{:<<>>, 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,
|
||||
@@ -575,7 +552,7 @@ defmodule Code.Normalizer do
|
||||
atom
|
||||
|> Atom.to_string()
|
||||
|> maybe_escape_literal(state)
|
||||
|> String.to_unsafe_atom()
|
||||
|> String.to_atom()
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(term, _) do
|
||||
|
||||
@@ -80,8 +80,7 @@ defmodule Code.Typespec do
|
||||
Returns all types available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+,
|
||||
`:nominal`).
|
||||
element is the type (`:typep`, `:type` and `:opaque`).
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
@@ -96,10 +95,9 @@ defmodule Code.Typespec do
|
||||
|
||||
types =
|
||||
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
|
||||
kind in [:opaque, :type, :nominal] do
|
||||
kind in [:opaque, :type] do
|
||||
cond do
|
||||
kind == :opaque -> {:opaque, type}
|
||||
kind == :nominal -> {:nominal, type}
|
||||
{name, length(args)} in exported_types -> {:type, type}
|
||||
true -> {:typep, type}
|
||||
end
|
||||
@@ -119,7 +117,7 @@ defmodule Code.Typespec do
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The specs will be in the Erlang
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
Abstract Format.
|
||||
"""
|
||||
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
|
||||
@@ -137,10 +135,10 @@ defmodule Code.Typespec do
|
||||
Returns all callbacks available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is the callback name and arity and the second is the callback.
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding BEAM file
|
||||
which can be located by the runtime system. The callbacks will be
|
||||
which can be located by the runtime system. The types will be
|
||||
in the Erlang Abstract Format.
|
||||
"""
|
||||
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
|
||||
@@ -193,8 +191,8 @@ defmodule Code.Typespec do
|
||||
|
||||
## To AST conversion
|
||||
|
||||
defp collect_vars({:ann_type, _anno, [_var, type]}) do
|
||||
collect_vars(type)
|
||||
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
|
||||
@@ -401,10 +399,10 @@ defmodule Code.Typespec do
|
||||
defp erl_to_ex_var(var) do
|
||||
case Atom.to_string(var) do
|
||||
<<"_", c::utf8, rest::binary>> ->
|
||||
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
|
||||
<<c::utf8, rest::binary>> ->
|
||||
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-19
@@ -152,7 +152,6 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec config(atom(), keyword()) :: keyword()
|
||||
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
||||
if not Keyword.keyword?(opts) do
|
||||
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
||||
@@ -199,7 +198,6 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec config(atom(), atom(), term()) :: keyword()
|
||||
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
||||
get_config!()
|
||||
|> __merge__([{root_key, [{key, opts}]}])
|
||||
@@ -227,7 +225,6 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec read_config(atom()) :: keyword() | nil
|
||||
def read_config(root_key) when is_atom(root_key) do
|
||||
get_config!()[root_key]
|
||||
end
|
||||
@@ -236,7 +233,7 @@ defmodule Config do
|
||||
Returns the environment this configuration file is executed on.
|
||||
|
||||
In Mix projects this function returns the environment this configuration
|
||||
file is executed on.
|
||||
file is executed on.
|
||||
In releases, returns the `MIX_ENV` specified when running `mix release`.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
@@ -287,8 +284,8 @@ defmodule Config do
|
||||
|
||||
In case the file doesn't exist, an error is raised.
|
||||
|
||||
If the file is relative, it will be expanded relative to the
|
||||
directory of the current configuration file.
|
||||
If file is a relative, it will be expanded relatively to the
|
||||
directory the current configuration file is in.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -380,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
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule Config.Provider do
|
||||
For example, imagine you want to list some basic configuration
|
||||
on Mix's built-in `config/runtime.exs` file, but you also want
|
||||
to support additional configuration files. To do so, you can add
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule Config.Reader do
|
||||
|
||||
For example, if you expect the target system to have a config file
|
||||
in an absolute path, you can add this inside the `def project` portion
|
||||
of your `mix.exs`:
|
||||
of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
+81
-234
@@ -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 """
|
||||
@@ -618,7 +604,7 @@ defmodule Enum do
|
||||
acc,
|
||||
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
|
||||
(acc -> {:cont, chunk, acc} | {:cont, acc})
|
||||
) :: [chunk]
|
||||
) :: Enumerable.t()
|
||||
when chunk: any
|
||||
def chunk_while(enumerable, acc, chunk_fun, after_fun) do
|
||||
{_, {res, acc}} =
|
||||
@@ -666,7 +652,7 @@ defmodule Enum do
|
||||
[1, [2], 3, 4, 5, 6]
|
||||
|
||||
"""
|
||||
@spec concat(Enumerable.t(Enumerable.t(elem))) :: [elem] when elem: term
|
||||
@spec concat(t) :: t
|
||||
def concat(enumerables)
|
||||
|
||||
def concat(list) when is_list(list) do
|
||||
@@ -681,8 +667,8 @@ defmodule Enum do
|
||||
Concatenates the enumerable on the `right` with the enumerable on the
|
||||
`left`.
|
||||
|
||||
This function behaves similarly to the `++/2` operator with proper
|
||||
lists, but applied to enumerables.
|
||||
This function produces the same result as the `++/2` operator
|
||||
for lists.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -693,7 +679,7 @@ defmodule Enum do
|
||||
[1, 2, 3, 4, 5, 6]
|
||||
|
||||
"""
|
||||
@spec concat(Enumerable.t(elem), Enumerable.t(elem)) :: [elem] when elem: term
|
||||
@spec concat(t, t) :: t
|
||||
def concat(left, right) when is_list(left) and is_list(right) do
|
||||
left ++ right
|
||||
end
|
||||
@@ -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
|
||||
@@ -859,10 +837,6 @@ defmodule Enum do
|
||||
|
||||
"""
|
||||
@spec dedup_by(t, (element -> term)) :: list
|
||||
def dedup_by([head | tail], fun) do
|
||||
dedup_by_list(tail, fun, fun.(head), [head])
|
||||
end
|
||||
|
||||
def dedup_by(enumerable, fun) do
|
||||
{list, _} = reduce(enumerable, {[], []}, R.dedup(fun))
|
||||
:lists.reverse(list)
|
||||
@@ -907,7 +881,7 @@ defmodule Enum do
|
||||
|
||||
def drop(enumerable, amount) when is_integer(amount) and amount < 0 do
|
||||
{count, fun} = slice_count_and_fun(enumerable, 1)
|
||||
amount = amount + count
|
||||
amount = Kernel.min(amount + count, count)
|
||||
|
||||
if amount > 0 do
|
||||
fun.(0, amount, 1)
|
||||
@@ -977,8 +951,8 @@ defmodule Enum do
|
||||
## Examples
|
||||
|
||||
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
|
||||
some
|
||||
example
|
||||
"some"
|
||||
"example"
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -1240,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
|
||||
@@ -1257,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 ->
|
||||
@@ -1288,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
|
||||
@@ -1446,7 +1417,7 @@ defmodule Enum do
|
||||
)
|
||||
|
||||
# Avoid warnings about Dict
|
||||
dict_module = String.to_unsafe_atom("Dict")
|
||||
dict_module = String.to_atom("Dict")
|
||||
|
||||
reduce(reverse(enumerable), dict, fn entry, categories ->
|
||||
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
|
||||
@@ -1522,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
|
||||
@@ -1606,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
|
||||
@@ -1883,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
|
||||
@@ -1950,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
|
||||
@@ -2027,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
|
||||
@@ -2068,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
|
||||
@@ -2135,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
|
||||
@@ -2189,65 +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()) :: {min :: element, max :: element}
|
||||
@spec min_max(t, (-> empty_result)) :: {min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
|
||||
{min :: element, max :: 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(list = [_ | _], empty_fallback) when is_function(empty_fallback, 0) do
|
||||
min_max_list(list)
|
||||
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.()
|
||||
@@ -2258,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
|
||||
@@ -2311,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
|
||||
|
||||
@@ -2352,14 +2241,14 @@ defmodule Enum do
|
||||
|
||||
"""
|
||||
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
|
||||
{min :: element, max :: element} | empty_result
|
||||
{element, element} | empty_result
|
||||
when empty_result: any
|
||||
@spec min_max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module(),
|
||||
(-> empty_result)
|
||||
) :: {min :: element, max :: element} | empty_result
|
||||
) :: {element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max_by(
|
||||
enumerable,
|
||||
@@ -2370,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)
|
||||
@@ -2401,19 +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_list([h | t]), do: min_max_list(t, h, h)
|
||||
|
||||
defp min_max_list([h | t], min, max) do
|
||||
cond do
|
||||
h < min -> min_max_list(t, h, max)
|
||||
max < h -> min_max_list(t, min, h)
|
||||
true -> min_max_list(t, min, max)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_list([], min, max), do: {min, max}
|
||||
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`.
|
||||
@@ -2692,7 +2569,7 @@ defmodule Enum do
|
||||
5050
|
||||
|
||||
"""
|
||||
@spec reduce_while(t, acc, (element, acc -> {:cont, acc} | {:halt, acc})) :: acc
|
||||
@spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
|
||||
def reduce_while(enumerable, acc, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
|
||||
end
|
||||
@@ -2935,7 +2812,8 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
defp slide_list_middle(list, 0, last, start_to_middle) do
|
||||
slide_list_last(list, last + 1, [], start_to_middle)
|
||||
{slid_range, tail} = slide_list_last(list, last + 1, [])
|
||||
slid_range ++ :lists.reverse(start_to_middle, tail)
|
||||
end
|
||||
|
||||
# You asked for a middle index off the end of the list... you get what we've got
|
||||
@@ -2943,16 +2821,16 @@ defmodule Enum do
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
|
||||
defp slide_list_last([h | t], last, acc, start_to_middle) when last > 0 do
|
||||
slide_list_last(t, last - 1, [h | acc], start_to_middle)
|
||||
defp slide_list_last([h | t], last, acc) when last > 0 do
|
||||
slide_list_last(t, last - 1, [h | acc])
|
||||
end
|
||||
|
||||
defp slide_list_last(rest, 0, acc, start_to_middle) do
|
||||
:lists.reverse(acc, :lists.reverse(start_to_middle, rest))
|
||||
defp slide_list_last(rest, 0, acc) do
|
||||
{:lists.reverse(acc), rest}
|
||||
end
|
||||
|
||||
defp slide_list_last([], _, acc, start_to_middle) do
|
||||
:lists.reverse(acc, :lists.reverse(start_to_middle))
|
||||
defp slide_list_last([], _, acc) do
|
||||
{:lists.reverse(acc), []}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4216,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]
|
||||
@@ -4329,24 +4202,11 @@ defmodule Enum do
|
||||
empty.()
|
||||
|
||||
_ ->
|
||||
# The endpoint shortcut is only valid for sorters consistent with
|
||||
# the natural integer order of the range elements, which is known
|
||||
# to hold for the default sorters; any other sorter traverses the
|
||||
# elements, seeded with the first one since the range is not empty
|
||||
if fun == (&<=/2) or fun == (&>=/2) do
|
||||
last = last - rem(last - first, step)
|
||||
last = last - rem(last - first, step)
|
||||
|
||||
case fun.(first, last) do
|
||||
true -> first
|
||||
false -> last
|
||||
end
|
||||
else
|
||||
reduce_range(first + step, last, step, first, fn element, acc ->
|
||||
case fun.(acc, element) do
|
||||
true -> acc
|
||||
false -> element
|
||||
end
|
||||
end)
|
||||
case fun.(first, last) do
|
||||
true -> first
|
||||
false -> last
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4449,9 +4309,10 @@ defmodule Enum do
|
||||
## any?/2 all?/2
|
||||
|
||||
defp predicate_list([h | t], initial, fun) do
|
||||
case !!fun.(h) do
|
||||
^initial -> predicate_list(t, initial, fun)
|
||||
_ -> not initial
|
||||
if !!fun.(h) == initial do
|
||||
predicate_list(t, initial, fun)
|
||||
else
|
||||
not initial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4462,9 +4323,10 @@ defmodule Enum do
|
||||
defp predicate_range(first, last, step, initial, fun)
|
||||
when step > 0 and first <= last
|
||||
when step < 0 and first >= last do
|
||||
case !!fun.(first) do
|
||||
^initial -> predicate_range(first + step, last, step, initial, fun)
|
||||
_ -> not initial
|
||||
if !!fun.(first) == initial do
|
||||
predicate_range(first + step, last, step, initial, fun)
|
||||
else
|
||||
not initial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4543,20 +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_by
|
||||
|
||||
defp dedup_by_list([head | tail], fun, prev, acc) do
|
||||
case fun.(head) do
|
||||
^prev -> dedup_by_list(tail, fun, prev, acc)
|
||||
new_val -> dedup_by_list(tail, fun, new_val, [head | acc])
|
||||
end
|
||||
dedup_list(tail, acc)
|
||||
end
|
||||
|
||||
defp dedup_by_list([], _fun, _prev, acc), do: :lists.reverse(acc)
|
||||
defp dedup_list([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
## drop
|
||||
|
||||
@@ -5150,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__}
|
||||
@@ -5205,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
|
||||
@@ -5237,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
|
||||
@@ -5261,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
|
||||
|
||||
+40
-23
@@ -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
|
||||
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
|
||||
iex> raise "oops!"
|
||||
** (RuntimeError) oops!
|
||||
|
||||
You should use this exception sparingly, since most of the time it might be
|
||||
You should use this exceptions sparingly, since most of the time it might be
|
||||
better to define your own exceptions specific to your application or library.
|
||||
Sometimes, however, there are situations in which you don't expect a condition to
|
||||
happen, but you want to give a meaningful error message if it does. In those cases,
|
||||
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
|
||||
|
||||
For example, this can happen if you try to create an atom that is too large:
|
||||
|
||||
iex> String.to_unsafe_atom(String.duplicate("a", 100_000))
|
||||
iex> String.to_atom(String.duplicate("a", 100_000))
|
||||
** (SystemLimitError) a system limit has been reached
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
@@ -1807,7 +1820,7 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
defp load_module({name, _path, _loaded?}) do
|
||||
name
|
||||
|> List.to_unsafe_atom()
|
||||
|> List.to_atom()
|
||||
|> Code.ensure_loaded()
|
||||
end
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2376,7 +2389,7 @@ defmodule File.Error do
|
||||
@moduledoc """
|
||||
An exception that is raised when a file operation fails.
|
||||
|
||||
For example, this exception is raised, when trying to read a nonexistent file:
|
||||
For example, this exception is raised, when trying to read a non existent file:
|
||||
|
||||
iex> File.read!("nonexistent_file.txt")
|
||||
** (File.Error) could not read file "nonexistent_file.txt": no such file or directory
|
||||
@@ -2409,7 +2422,7 @@ defmodule File.CopyError do
|
||||
@moduledoc """
|
||||
An exception that is raised when copying a file fails.
|
||||
|
||||
For example, this exception is raised when trying to copy to a file or directory that isn't present:
|
||||
For example, this exception is raised when trying to copy to file or directory that isn't present:
|
||||
|
||||
iex> File.cp_r!("non_existent", "source_dir/subdir")
|
||||
** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory
|
||||
@@ -2477,7 +2490,7 @@ defmodule File.LinkError do
|
||||
@moduledoc """
|
||||
An exception that is raised when linking a file fails.
|
||||
|
||||
For example, this exception is raised when trying to link to a file that isn't present:
|
||||
For example, this exception is raised when trying to link to file that isn't present:
|
||||
|
||||
iex> File.ln!("existing.txt", "link.txt")
|
||||
** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory
|
||||
@@ -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
|
||||
|
||||
+34
-131
@@ -317,7 +317,7 @@ defmodule File do
|
||||
directories of `path`
|
||||
* `:enospc` - there is no space left on the device
|
||||
* `:enotdir` - a component of `path` is not a directory
|
||||
* `:eperm` - missed required permissions
|
||||
* `:eperm` - missed required permisions
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -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
|
||||
|
||||
"""
|
||||
@@ -722,7 +706,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
:ok <- write(path, "", [:append]),
|
||||
do: :elixir_utils.change_universal_time(path, time)
|
||||
end
|
||||
|
||||
@@ -730,7 +714,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
:ok <- write(path, "", [:append]),
|
||||
do: :elixir_utils.change_posix_time(path, time)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -897,7 +881,7 @@ defmodule File do
|
||||
File.copy("non_existing.txt", "copy.txt")
|
||||
#=> {:error, :enoent}
|
||||
"""
|
||||
@spec copy(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
|
||||
@spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
|
||||
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
|
||||
def copy(source, destination, bytes_count \\ :infinity) do
|
||||
source = normalize_path_or_io_device(source)
|
||||
@@ -918,7 +902,7 @@ defmodule File do
|
||||
File.copy!("non_existing.txt", "copy.txt")
|
||||
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
|
||||
"""
|
||||
@spec copy!(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
|
||||
@spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
|
||||
non_neg_integer
|
||||
def copy!(source, destination, bytes_count \\ :infinity) do
|
||||
case copy(source, destination, bytes_count) do
|
||||
@@ -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,28 +1221,15 @@ 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
|
||||
# we don't have a way to make a distinction between a non-existing src
|
||||
# or dest being a non-existing dir in the case of :enoent,
|
||||
# but we already know that src exists here.
|
||||
{:error, :enoent, _} -> {:error, :enoent, dest}
|
||||
other -> other
|
||||
end
|
||||
do_cp_file(src, dest, on_conflict, acc)
|
||||
|
||||
{: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 +1243,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 +1256,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
{:ok, _} ->
|
||||
acc
|
||||
{:error, :eio, src}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, src}
|
||||
@@ -1357,13 +1264,14 @@ 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
|
||||
|
||||
defp copy_file_mode(src, dest) do
|
||||
with {:ok, src_fileinfo} <- stat(src) do
|
||||
chmod(dest, src_fileinfo.mode)
|
||||
with {:ok, dest_fileinfo} <- stat(dest),
|
||||
{:ok, src_fileinfo} <- stat(src) do
|
||||
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2220,7 +2128,7 @@ defmodule File do
|
||||
type. If you pass, for example, `[encoding: :utf8]` or
|
||||
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
|
||||
will use `IO.write/2` and the `String.Chars` protocol to convert the data.
|
||||
See `IO.binwrite/2` and `IO.write/2`.
|
||||
See `IO.binwrite/2` and `IO.write/2` .
|
||||
|
||||
One may also consider passing the `:delayed_write` option if the stream
|
||||
is meant to be written to under a tight loop.
|
||||
@@ -2251,12 +2159,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
|
||||
|
||||
|
||||
@@ -18,13 +18,7 @@ defmodule File.Stream do
|
||||
|
||||
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
path: Path.t(),
|
||||
modes: [term()],
|
||||
line_or_bytes: :line | pos_integer(),
|
||||
raw: boolean(),
|
||||
node: node()
|
||||
}
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@doc false
|
||||
def __build__(path, line_or_bytes, modes) do
|
||||
@@ -125,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)}
|
||||
@@ -235,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
|
||||
|
||||
+142
-191
@@ -25,7 +25,7 @@ defmodule Float do
|
||||
and arithmetic due to the fact most decimal fractions cannot be
|
||||
represented by a floating-point binary and most operations are not exact,
|
||||
but operate on approximations. Those issues are not specific
|
||||
to Elixir, they are a property of floating-point representation itself.
|
||||
to Elixir, they are a property of floating point representation itself.
|
||||
|
||||
For example, the numbers 0.1 and 0.01 are two of them, what means the result
|
||||
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
|
||||
@@ -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/)
|
||||
|
||||
"""
|
||||
@@ -167,73 +167,51 @@ defmodule Float do
|
||||
parse_unsigned(binary)
|
||||
end
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, false)
|
||||
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, false, false, [digit])
|
||||
|
||||
defp parse_unsigned(binary) when is_binary(binary), do: :error
|
||||
|
||||
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, dot?)
|
||||
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, dot?, e?, [digit | acc])
|
||||
|
||||
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, true)
|
||||
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, false, [digit, ?. | acc])
|
||||
|
||||
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?)
|
||||
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
|
||||
when exp_marker in ~c"eE" and digit in ?0..?9,
|
||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
||||
do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
|
||||
|
||||
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?)
|
||||
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
|
||||
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
|
||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
||||
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
|
||||
|
||||
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?)
|
||||
|
||||
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
||||
do: parse_exponent(binary, exp_pos, rest, dot?)
|
||||
|
||||
defp parse_exponent(binary, exp_pos, rest, dot?),
|
||||
do: finish_exponent(binary, exp_pos, rest, dot?)
|
||||
|
||||
defp finish_mantissa(binary, rest, _dot? = true) do
|
||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
||||
# 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()
|
||||
|> :erlang.list_to_float()
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
else
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
# Bare integer: * 1.0 casts to the nearest float without building a new binary,
|
||||
# and raises ArithmeticError on overflow (for example a 400-digit integer).
|
||||
defp finish_mantissa(binary, rest, _dot? = false) do
|
||||
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
|
||||
rescue
|
||||
ArithmeticError -> :error
|
||||
defp parse_unsigned(rest, dot?, false = _e?, acc) do
|
||||
float =
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
|> :erlang.list_to_float()
|
||||
|
||||
{float, rest}
|
||||
end
|
||||
|
||||
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
|
||||
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
|
||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
|
||||
# form a valid float literal.
|
||||
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
|
||||
len = byte_size(binary) - byte_size(rest)
|
||||
|
||||
literal =
|
||||
IO.iodata_to_binary([
|
||||
:binary.part(binary, 0, exp_pos),
|
||||
".0",
|
||||
:binary.part(binary, exp_pos, len - exp_pos)
|
||||
])
|
||||
|
||||
{:erlang.binary_to_float(literal), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
defp consumed(binary, ""), do: binary
|
||||
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
|
||||
defp add_dot(acc, true), do: acc
|
||||
defp add_dot(acc, false), do: [?0, ?. | acc]
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest float less than or equal to `number`.
|
||||
@@ -286,7 +264,7 @@ defmodule Float do
|
||||
@doc """
|
||||
Rounds a float to the smallest float greater than or equal to `number`.
|
||||
|
||||
`ceil/2` also accepts a precision to round a floating-point value up
|
||||
`ceil/2` also accepts a precision to round a floating-point value down
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
|
||||
The operation is performed on the binary floating point, without a
|
||||
@@ -355,7 +333,7 @@ defmodule Float do
|
||||
and therefore the number above is internally represented as 5.567499999,
|
||||
which explains the behavior above. If you want exact rounding for decimals,
|
||||
you must use a decimal library. The behavior above is also in accordance
|
||||
with reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
to reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
Decimal-Binary Conversions" by David M. Gay.
|
||||
|
||||
## Examples
|
||||
@@ -377,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 or float === -0.0, do: float
|
||||
def round(float, 0) when float == 0.0, do: float
|
||||
|
||||
def round(float, 0) when is_float(float) do
|
||||
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
|
||||
@@ -396,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.
|
||||
@@ -705,7 +656,7 @@ defmodule Float do
|
||||
end
|
||||
|
||||
defp invalid_precision_message(precision) do
|
||||
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}"
|
||||
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
|
||||
end
|
||||
|
||||
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
|
||||
|
||||
@@ -69,6 +69,7 @@ defmodule Function do
|
||||
| :name
|
||||
| :new_index
|
||||
| :new_uniq
|
||||
| :pid
|
||||
| :type
|
||||
| :uniq
|
||||
|
||||
@@ -111,6 +112,8 @@ defmodule Function do
|
||||
When `fun` is an anonymous function (that is, the type is `:local`), the following
|
||||
additional keys are returned:
|
||||
|
||||
* `:pid` - PID of the process that originally created the function.
|
||||
|
||||
* `:index` - (integer) an index into the module function table.
|
||||
|
||||
* `:new_index` - (integer) an index into the module function table.
|
||||
@@ -156,7 +159,7 @@ defmodule Function do
|
||||
`:module`, `:name`, `:arity`, `:env`, or `:type`.
|
||||
|
||||
For anonymous functions, there is also information about any of the
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`.
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
|
||||
For a named function, the value of any of these items is always the
|
||||
atom `:undefined`.
|
||||
|
||||
@@ -176,6 +179,8 @@ defmodule Function do
|
||||
iex> fun = &String.length/1
|
||||
iex> Function.info(fun, :name)
|
||||
{:name, :length}
|
||||
iex> Function.info(fun, :pid)
|
||||
{:pid, :undefined}
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule GenEvent do
|
||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||
team; it provides a tool to implement systems that exchange events in a
|
||||
demand-driven way with built-in support for back-pressure. See the [GenStage
|
||||
documentation](https://gen-stage.hexdocs.pm) for more information.
|
||||
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||
|
||||
### `:gen_event`
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -1158,7 +1117,7 @@ defmodule GenServer do
|
||||
|
||||
## Timeouts
|
||||
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds to wait for a reply, or the atom `:infinity` to wait
|
||||
indefinitely. The default value is `5000`. If no reply is received within
|
||||
the specified time, the function call fails and the caller exits. If the
|
||||
|
||||
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def reduce(dict, acc, fun) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module = String.to_atom("HashDict")
|
||||
module.reduce(dict, acc, fun)
|
||||
end
|
||||
|
||||
def member?(dict, {key, value}) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module = String.to_atom("HashDict")
|
||||
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
|
||||
end
|
||||
|
||||
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def count(dict) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module = String.to_atom("HashDict")
|
||||
{:ok, module.size(dict)}
|
||||
end
|
||||
|
||||
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module = String.to_atom("HashDict")
|
||||
|
||||
collector_fun = fn
|
||||
dict, {:cont, {key, value}} -> module.put(dict, key, value)
|
||||
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
|
||||
|
||||
def inspect(dict, opts) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module = String.to_atom("HashDict")
|
||||
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
|
||||
|
||||
def reduce(set, acc, fun) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module = String.to_atom("HashSet")
|
||||
module.reduce(set, acc, fun)
|
||||
end
|
||||
|
||||
def member?(set, term) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module = String.to_atom("HashSet")
|
||||
{:ok, module.member?(set, term)}
|
||||
end
|
||||
|
||||
def count(set) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module = String.to_atom("HashSet")
|
||||
{:ok, module.size(set)}
|
||||
end
|
||||
|
||||
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module = String.to_atom("HashSet")
|
||||
|
||||
collector_fun = fn
|
||||
set, {:cont, term} -> module.put(set, term)
|
||||
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
|
||||
|
||||
def inspect(set, opts) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module = String.to_atom("HashSet")
|
||||
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
+11
-43
@@ -92,7 +92,7 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
inspect(%Point{x: 1})
|
||||
#=> %Point{x: 1, y: 0}
|
||||
%Point{x: 1, y: 0}
|
||||
|
||||
## Custom implementation
|
||||
|
||||
@@ -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
|
||||
@@ -387,20 +389,13 @@ defimpl Inspect, for: List do
|
||||
close = color_doc("]", :list, opts)
|
||||
|
||||
cond do
|
||||
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
|
||||
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
{split, tail} =
|
||||
if is_integer(printable_limit) do
|
||||
case Enum.split(term, printable_limit) do
|
||||
{split, []} -> {split, []}
|
||||
{split, _} -> {split, " ++ ..."}
|
||||
end
|
||||
else
|
||||
{term, []}
|
||||
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
inspected =
|
||||
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
|
||||
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
|
||||
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
|
||||
end
|
||||
|
||||
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
|
||||
inspected = [?~, ?c, ?", escaped, ?" | tail]
|
||||
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
|
||||
|
||||
keyword?(term) ->
|
||||
@@ -414,19 +409,6 @@ defimpl Inspect, for: List do
|
||||
end
|
||||
end
|
||||
|
||||
defp unicode_list?(_, 0), do: true
|
||||
|
||||
defp unicode_list?([char | rest], counter)
|
||||
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
|
||||
do: unicode_list?(rest, decrement(counter))
|
||||
|
||||
defp unicode_list?([], _counter), do: true
|
||||
defp unicode_list?(_, _counter), do: false
|
||||
|
||||
@compile {:inline, decrement: 1}
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
|
||||
@doc false
|
||||
def keyword({key, value}, opts) do
|
||||
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
|
||||
@@ -436,8 +418,8 @@ defimpl Inspect, for: List do
|
||||
|
||||
@doc false
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case Atom.to_string(key) do
|
||||
"Elixir." <> _ -> false
|
||||
case Atom.to_charlist(key) do
|
||||
[?E, ?l, ?i, ?x, ?i, ?r, ?.] ++ _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
@@ -587,7 +569,6 @@ defimpl Inspect, for: Regex do
|
||||
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
|
||||
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
|
||||
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
|
||||
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
|
||||
defp translate_options([], acc), do: acc
|
||||
defp translate_options(_t, _acc), do: :error
|
||||
|
||||
@@ -689,11 +670,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 +697,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
|
||||
|
||||
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
|
||||
is `:decimal` and if it is printable, otherwise in bit syntax. See
|
||||
`String.printable?/1` to learn when a string is printable.
|
||||
|
||||
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
|
||||
non-printable code points will be escaped. Other lists will be printed as lists.
|
||||
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
|
||||
non-printable elements will be escaped.
|
||||
|
||||
When `:as_lists` all lists will be printed as lists.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -108,7 +69,7 @@ defmodule Integer do
|
||||
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to the power of `exponent`.
|
||||
Computes `base` raised to power of `exponent`.
|
||||
|
||||
Both `base` and `exponent` must be integers.
|
||||
The exponent must be zero or positive.
|
||||
@@ -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 is_integer(b) and b > 0, do: {b, 0, 1}
|
||||
def extended_gcd(0, b) when is_integer(b) and b < 0, do: {-b, 0, -1}
|
||||
def extended_gcd(a, 0) when is_integer(a) and a > 0, do: {a, 1, 0}
|
||||
def extended_gcd(a, 0) when is_integer(a) and a < 0, do: {-a, -1, 0}
|
||||
def extended_gcd(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)
|
||||
|
||||
+7
-31
@@ -128,7 +128,7 @@ defmodule IO do
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
|
||||
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, String.Chars.t()}]
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
|
||||
|
||||
@typedoc """
|
||||
Stacktrace information as keyword options for `warn/2`.
|
||||
@@ -152,9 +152,9 @@ defmodule IO do
|
||||
|
||||
The `device` is iterated as specified by the `line_or_chars` argument:
|
||||
|
||||
* if `line_or_chars` is an integer, it is the number of Unicode
|
||||
code points to be retrieved for devices open in Unicode/utf8 mode.
|
||||
Otherwise, it is the number of raw bytes to be retrieved.
|
||||
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
|
||||
iterated by that number of bytes. This should be the preferred mode for reading
|
||||
non-textual inputs.
|
||||
|
||||
* if `line_or_chars` is `:line`, the device is iterated line by line.
|
||||
CRLF newlines ("\r\n") are automatically normalized to "\n".
|
||||
@@ -511,8 +511,7 @@ defmodule IO do
|
||||
"""
|
||||
@spec inspect(device, item, inspect_opts) :: item when item: var
|
||||
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
|
||||
{label, opts} = Keyword.pop(opts, :label)
|
||||
label = if label, do: [to_chardata(label), ": "], else: []
|
||||
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
|
||||
opts = Inspect.Opts.new(opts)
|
||||
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
|
||||
chardata = Inspect.Algebra.format(doc, opts.width)
|
||||
@@ -521,7 +520,7 @@ defmodule IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of characters from IO device `:stdio`.
|
||||
Gets a number of bytes from IO device `:stdio`.
|
||||
|
||||
If `:stdio` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
@@ -549,7 +548,7 @@ defmodule IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of characters from the IO `device`.
|
||||
Gets a number of bytes from the IO `device`.
|
||||
|
||||
If the IO `device` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
@@ -796,29 +795,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
|
||||
|
||||
@@ -111,15 +111,13 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defsequence = fn name, code, terminator ->
|
||||
sequence = "\e[#{code}#{terminator}"
|
||||
|
||||
@spec unquote(name)() :: String.t()
|
||||
def unquote(name)() do
|
||||
unquote(sequence)
|
||||
"\e[#{unquote(code)}#{unquote(terminator)}"
|
||||
end
|
||||
|
||||
defp format_sequence(unquote(name)) do
|
||||
unquote(sequence)
|
||||
unquote(name)()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -161,7 +159,7 @@ defmodule IO.ANSI do
|
||||
|
||||
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
|
||||
@doc "Sets alternative font #{font_n}."
|
||||
defsequence.(String.to_unsafe_atom("font_#{font_n}"), font_n + 10, "m")
|
||||
defsequence.(:"font_#{font_n}", font_n + 10, "m")
|
||||
end
|
||||
|
||||
@doc "Normal color or intensity."
|
||||
@@ -195,13 +193,13 @@ defmodule IO.ANSI do
|
||||
defsequence.(color, code + 30, "m")
|
||||
|
||||
@doc "Sets foreground color to light #{color}."
|
||||
defsequence.(String.to_unsafe_atom("light_#{color}"), code + 90, "m")
|
||||
defsequence.(:"light_#{color}", code + 90, "m")
|
||||
|
||||
@doc "Sets background color to #{color}."
|
||||
defsequence.(String.to_unsafe_atom("#{color}_background"), code + 40, "m")
|
||||
defsequence.(:"#{color}_background", code + 40, "m")
|
||||
|
||||
@doc "Sets background color to light #{color}."
|
||||
defsequence.(String.to_unsafe_atom("light_#{color}_background"), code + 100, "m")
|
||||
defsequence.(:"light_#{color}_background", code + 100, "m")
|
||||
end
|
||||
|
||||
@doc "Default text color."
|
||||
@@ -334,7 +332,7 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, true, true) do
|
||||
[acc | reset()]
|
||||
[acc | IO.ANSI.reset()]
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, _emit?, _append_reset) do
|
||||
|
||||
@@ -673,7 +673,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
# Characters that can mark the beginning or the end of a word.
|
||||
# Only support the most common ones at this moment.
|
||||
@delimiters [?\s, ?', ?", ?!, ??, ?,, ?:, ?;, ?/, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
|
||||
|
||||
### Inline start
|
||||
|
||||
@@ -31,7 +31,7 @@ defmodule IO.Stream do
|
||||
@type t :: %__MODULE__{
|
||||
device: IO.device(),
|
||||
raw: boolean(),
|
||||
line_or_bytes: :line | pos_integer()
|
||||
line_or_bytes: :line | non_neg_integer()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
||||
+17
-23
@@ -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 caution.
|
||||
> 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
|
||||
|
||||
@@ -175,7 +170,6 @@ defimpl JSON.Encoder, for: Map do
|
||||
|
||||
# Erlang supports only numbers, binaries, and atoms as keys,
|
||||
# we support anything that implements the String.Chars protocol.
|
||||
@compile inline: [key: 2]
|
||||
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
|
||||
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
|
||||
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
|
||||
@@ -409,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)}}
|
||||
@@ -529,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)
|
||||
|
||||
+115
-146
@@ -142,7 +142,7 @@ defmodule Kernel do
|
||||
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
|
||||
guards, and extensions
|
||||
* [Syntax reference](syntax-reference.md) - the language syntax reference
|
||||
* [Typespecs reference](typespecs.md) - types and function specifications, including list of types
|
||||
* [Typespecs reference](typespecs.md)- types and function specifications, including list of types
|
||||
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
|
||||
|
||||
## Guards
|
||||
@@ -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
|
||||
@@ -3088,7 +3079,7 @@ defmodule Kernel do
|
||||
@doc """
|
||||
Pops a key from the given nested structure.
|
||||
|
||||
Uses the `Access` behaviour to traverse the structures
|
||||
Uses the `Access` protocol to traverse the structures
|
||||
according to the given `keys`, unless the `key` is a
|
||||
function. If the key is a function, it will be invoked
|
||||
as specified in `get_and_update_in/3`.
|
||||
@@ -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))
|
||||
)
|
||||
@@ -4960,7 +4960,6 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec binary_slice(binary, integer, non_neg_integer) :: binary
|
||||
def binary_slice(binary, start, size)
|
||||
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
|
||||
total = byte_size(binary)
|
||||
@@ -5034,7 +5033,6 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec binary_slice(binary, Range.t()) :: binary
|
||||
def binary_slice(binary, first..last//step)
|
||||
when is_binary(binary) and step > 0 do
|
||||
total = byte_size(binary)
|
||||
@@ -5149,7 +5147,7 @@ defmodule Kernel do
|
||||
warning saying that a module has been redefined.
|
||||
|
||||
There are some modules that Elixir does not currently implement but it
|
||||
may implement in the future. Those modules are reserved and defining
|
||||
may be implement in the future. Those modules are reserved and defining
|
||||
them will result in a compilation error:
|
||||
|
||||
defmodule Any do
|
||||
@@ -5216,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: []
|
||||
|
||||
@@ -5250,12 +5243,12 @@ defmodule Kernel do
|
||||
# defmodule Alias nested
|
||||
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
|
||||
module = :elixir_aliases.concat([env.module, h])
|
||||
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h))
|
||||
alias = String.to_atom("Elixir." <> Atom.to_string(h))
|
||||
opts = [as: alias, warn: false]
|
||||
|
||||
case t do
|
||||
[] -> {module, module, opts}
|
||||
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts}
|
||||
_ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5562,7 +5555,7 @@ defmodule Kernel do
|
||||
when the struct is printed:
|
||||
|
||||
defmodule User do
|
||||
@derive {Inspect, only: [:name]}
|
||||
@derive {Inspect, only: :name}
|
||||
defstruct name: nil, age: nil
|
||||
end
|
||||
|
||||
@@ -5589,9 +5582,6 @@ defmodule Kernel do
|
||||
defstruct name: nil, age: 10 + 11
|
||||
end
|
||||
|
||||
`@enforce_keys` must be set to an atom or a list of unique atoms,
|
||||
all of which must name fields defined by `defstruct/1`
|
||||
|
||||
Now trying to build a struct without the name key will fail:
|
||||
|
||||
%User{age: 21}
|
||||
@@ -5604,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
|
||||
@@ -5876,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)
|
||||
@@ -5892,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()
|
||||
@@ -6283,7 +6270,7 @@ defmodule Kernel do
|
||||
step through the code it sees). For general stepping, you can set breakpoints
|
||||
using `IEx.break!/4`.
|
||||
|
||||
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints).
|
||||
For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
|
||||
|
||||
## Configuring the debug function
|
||||
|
||||
@@ -6330,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
|
||||
|
||||
@@ -6382,7 +6361,7 @@ defmodule Kernel do
|
||||
|
||||
### Passing timeouts
|
||||
|
||||
You can also pass timeouts directly to this function, that is, milliseconds or
|
||||
You can also pass timeouts directly to this functions, that is, milliseconds or
|
||||
the atom `:infinity`. In this case, this function just returns the given argument.
|
||||
|
||||
## Examples
|
||||
@@ -6399,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
|
||||
@@ -6427,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
|
||||
|
||||
@@ -6648,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
|
||||
@@ -6902,7 +6871,7 @@ defmodule Kernel do
|
||||
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
|
||||
when alias >= ?A and alias <= ?Z do
|
||||
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
|
||||
{String.to_unsafe_atom("Elixir." <> last_part), string}
|
||||
{String.to_atom("Elixir." <> last_part), string}
|
||||
end
|
||||
|
||||
defp maybe_atomize_calendar(_last_part, string) do
|
||||
@@ -7013,7 +6982,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> :lists.map(&String.to_unsafe_atom/1, parts)
|
||||
?a -> :lists.map(&String.to_atom/1, parts)
|
||||
?c -> :lists.map(&String.to_charlist/1, parts)
|
||||
end
|
||||
|
||||
@@ -7022,7 +6991,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts)))
|
||||
?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
|
||||
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
|
||||
end
|
||||
end
|
||||
@@ -7056,7 +7025,7 @@ defmodule Kernel do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, #{exp} is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -49,7 +49,7 @@ defmodule Kernel.CLI do
|
||||
|
||||
@doc """
|
||||
Runs the given function by catching any failure
|
||||
and printing them to stderr. `at_exit` hooks are
|
||||
and printing them to stdout. `at_exit` hooks are
|
||||
also invoked before exiting.
|
||||
|
||||
This function is used by Elixir's CLI and also
|
||||
@@ -99,14 +99,9 @@ defmodule Kernel.CLI do
|
||||
Shared helper for error formatting on CLI tools.
|
||||
"""
|
||||
def format_error(kind, reason, stacktrace) do
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
[banner, rest]
|
||||
end
|
||||
|
||||
defp format_error_parts(kind, reason, stacktrace) do
|
||||
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
|
||||
|
||||
banner =
|
||||
iodata =
|
||||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
formatted = Exception.format_banner(kind, reason, stacktrace)
|
||||
@@ -117,7 +112,7 @@ defmodule Kernel.CLI do
|
||||
Exception.format_banner(kind, blamed, stacktrace)
|
||||
end
|
||||
|
||||
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]}
|
||||
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -184,8 +179,7 @@ defmodule Kernel.CLI do
|
||||
## Error handling
|
||||
|
||||
defp print_error(kind, reason, stacktrace) do
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
|
||||
IO.write(:stderr, format_error(kind, reason, stacktrace))
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
|
||||
@@ -206,7 +200,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++
|
||||
[:elixir_dispatch, :elixir_expand, :elixir_lexical] ++
|
||||
[:elixir_def, :elixir_dispatch, :elixir_expand, :elixir_lexical] ++
|
||||
[:elixir_map, :elixir_module] ++
|
||||
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
|
||||
[Kernel.ErrorHandler, Module.ParallelChecker]
|
||||
@@ -344,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
|
||||
|
||||
@@ -433,7 +427,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
|
||||
node = List.to_unsafe_atom(node)
|
||||
node = List.to_atom(node)
|
||||
|
||||
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
|
||||
_ = :net_kernel.connect_node(node)
|
||||
|
||||
@@ -21,18 +21,9 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :references, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate a require
|
||||
that 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
|
||||
that must be warned if unused.
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_alias(pid, meta, alias, module) do
|
||||
:gen_server.cast(pid, {:warn_alias, alias, meta})
|
||||
@@ -41,7 +32,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an import
|
||||
that must be warned if unused.
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_import(pid, module) do
|
||||
:gen_server.cast(pid, {:warn_import, module})
|
||||
@@ -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, [Path.t()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [{module(), Path.t()}], [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)
|
||||
@@ -100,8 +100,6 @@ defmodule Kernel.ParallelCompiler do
|
||||
This function allows a developer to perform such tasks.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec pmap(Enumerable.t(input), (input -> output)) :: [output]
|
||||
when input: term, output: term
|
||||
def pmap(collection, fun) when is_function(fun, 1) do
|
||||
ref = make_ref()
|
||||
|
||||
@@ -173,25 +171,25 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:each_long_verification` (since v1.19.0) - for each file that takes more
|
||||
than a given timeout (see the `:long_verification_threshold` option) to
|
||||
verify, invoke this callback passing the module as its argument (and
|
||||
compile, invoke this callback passing the module as its argument (and
|
||||
optionally the PID of the process verifying the module)
|
||||
|
||||
* `:each_module` - for each module compiled, invokes the callback passing
|
||||
the file, module and the module bytecode
|
||||
|
||||
* `:each_cycle` - invoked after each compilation cycle and should return one
|
||||
of the following values:
|
||||
* `:each_cycle` - after the given files are compiled, invokes this function
|
||||
that should return the following values:
|
||||
* `{:compile, modules, warnings}` - to continue compilation with a list of
|
||||
further module files to compile
|
||||
further modules to compile
|
||||
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
|
||||
of `{module, path}` pairs because dependent modules have changed
|
||||
of modules because dependent modules have changed
|
||||
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) to check for files
|
||||
taking too long to compile. For each file that exceeds the threshold, the
|
||||
`:each_long_compilation` callback is invoked. Defaults to `10` seconds.
|
||||
|
||||
* `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to
|
||||
check for modules taking too long to verify. For each module that exceeds the
|
||||
check for modules taking too long to compile. For each module that exceeds the
|
||||
threshold, the `:each_long_verification` callback is invoked. Defaults to
|
||||
`10` seconds.
|
||||
|
||||
@@ -199,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
|
||||
@@ -214,7 +209,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backward compatibility reasons.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
@@ -271,7 +266,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backward compatibility reasons.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@@ -341,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, %{}, %{}, [], %{}, [], [], %{
|
||||
@@ -365,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)
|
||||
@@ -393,75 +380,29 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, {:compile, path}, state) when map_size(result) > 0 do
|
||||
profile(state, "writing modules to disk", fn ->
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
timestamp = state.beam_timestamp
|
||||
defp write_module_binaries(result, {:compile, path}, timestamp) do
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
|
||||
# We fan-out the writes as that improves performance
|
||||
# when writing hundreds of beam files. This is cheap as
|
||||
# we only transfer atoms and binaries across processes.
|
||||
pool_size = min(map_size(result), state.schedulers)
|
||||
|
||||
pool_list =
|
||||
for _ <- 1..pool_size do
|
||||
spawn_link(fn -> write_loop(path, timestamp) end)
|
||||
end
|
||||
|
||||
pool_tuple = List.to_tuple(pool_list)
|
||||
|
||||
{modules, _} =
|
||||
Enum.flat_map_reduce(result, 0, fn
|
||||
{{:module, module}, {binary, _}}, scheduler when is_binary(binary) ->
|
||||
send(elem(pool_tuple, scheduler), {:write, module, binary})
|
||||
{[module], rem(scheduler + 1, pool_size)}
|
||||
|
||||
_, scheduler ->
|
||||
{[], scheduler}
|
||||
end)
|
||||
|
||||
pool_refs =
|
||||
for pid <- pool_list do
|
||||
ref = Process.monitor(pid)
|
||||
send(pid, :done)
|
||||
ref
|
||||
end
|
||||
|
||||
for ref <- pool_refs do
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
modules
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _state) do
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
|
||||
end
|
||||
|
||||
defp write_loop(path, timestamp) do
|
||||
receive do
|
||||
{:write, module, binary} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary, [:raw])
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
write_loop(path, timestamp)
|
||||
|
||||
:done ->
|
||||
:ok
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result do
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
module
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _timestamp) do
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verify_modules(result, compile_warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state.output, state)
|
||||
profile(state, "after compile callback", state.after_compile)
|
||||
modules = write_module_binaries(result, state.output, state.beam_timestamp)
|
||||
_ = state.after_compile.()
|
||||
|
||||
{runtime_warnings, errors} =
|
||||
runtime_warnings =
|
||||
if state.verification? do
|
||||
profile(
|
||||
state,
|
||||
@@ -472,19 +413,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}
|
||||
@@ -591,11 +524,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} ->
|
||||
@@ -842,7 +775,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)
|
||||
|
||||
@@ -900,7 +833,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} ->
|
||||
|
||||
@@ -768,8 +768,8 @@ defmodule Kernel.SpecialForms do
|
||||
It is used in typespecs to specify the type of a variable,
|
||||
function or of a type itself:
|
||||
|
||||
@type num :: integer | float
|
||||
@spec add(num, num) :: num
|
||||
@type number :: integer | float
|
||||
@spec add(number, number) :: number
|
||||
|
||||
It may also be used in bit strings to specify the type
|
||||
of a given bit segment:
|
||||
@@ -1333,9 +1333,9 @@ defmodule Kernel.SpecialForms do
|
||||
sum(1, value, 3)
|
||||
end
|
||||
|
||||
However, the code above does not work as expected, because this
|
||||
injects the representation of the `value` variable,
|
||||
not its contents:
|
||||
|
||||
Which the argument for the `:sum` function call is not the
|
||||
expected result:
|
||||
|
||||
{:sum, [], [1, {:value, [], Elixir}, 3]}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -2077,9 +2089,9 @@ defmodule Kernel.SpecialForms do
|
||||
iex> try do
|
||||
...> 1 / 0
|
||||
...> rescue
|
||||
...> x in [ArithmeticError] -> {:rescued, Exception.message(x)}
|
||||
...> x in [ArithmeticError] -> [:rescued, is_exception(x)]
|
||||
...> end
|
||||
{:rescued, "bad argument in arithmetic expression"}
|
||||
[:rescued, true]
|
||||
|
||||
Rescue different errors with separate 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
|
||||
)
|
||||
@@ -686,13 +676,6 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :range, [left, right]}, state}
|
||||
end
|
||||
|
||||
defp typespec({:..//, _meta, [_first, _last, _step]} = range, _vars, caller, _state) do
|
||||
compile_error(
|
||||
caller,
|
||||
"ranges with steps are not supported in typespecs, got: #{Macro.to_string(range)}"
|
||||
)
|
||||
end
|
||||
|
||||
# Handle special forms
|
||||
defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do
|
||||
typespec(caller.module, vars, caller, state)
|
||||
@@ -867,29 +850,29 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :nonempty_string, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
if type == :char_list do
|
||||
warning = "the char_list() type is deprecated, use charlist()"
|
||||
IO.warn(warning, caller)
|
||||
end
|
||||
|
||||
remote_typespec(:charlist, meta, [], vars, caller, state)
|
||||
typespec(quote(do: :elixir.charlist()), vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state)
|
||||
defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:struct, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:struct, meta, [], vars, caller, state)
|
||||
defp typespec({:struct, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.struct()), vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do
|
||||
remote_typespec(:as_boolean, meta, [arg], vars, caller, state)
|
||||
defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
remote_typespec(:keyword, meta, args, vars, caller, state)
|
||||
defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
@@ -1014,10 +997,6 @@ defmodule Kernel.Typespec do
|
||||
{{:remote_type, location(meta), [remote, name, args]}, state}
|
||||
end
|
||||
|
||||
defp remote_typespec(name, meta, args, vars, caller, state) do
|
||||
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
|
||||
end
|
||||
|
||||
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
|
||||
defp collect_union(v), do: [v]
|
||||
|
||||
|
||||
@@ -162,8 +162,16 @@ defmodule Kernel.Utils do
|
||||
# TODO: Make it raise on v2.0
|
||||
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
|
||||
|
||||
field_map = :maps.from_list(fields)
|
||||
struct = :maps.put(:__struct__, module, field_map)
|
||||
foreach = fn
|
||||
key when is_atom(key) ->
|
||||
:ok
|
||||
|
||||
key ->
|
||||
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
|
||||
end
|
||||
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
struct = :maps.from_list([__struct__: module] ++ fields)
|
||||
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
|
||||
|
||||
body =
|
||||
@@ -207,7 +215,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
case enforce_keys -- :maps.keys(field_map) do
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
[] ->
|
||||
mapper = fn {key, val} ->
|
||||
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
|
||||
@@ -217,9 +225,10 @@ defmodule Kernel.Utils do
|
||||
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
|
||||
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
|
||||
|
||||
invalid_keys ->
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
"unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}"
|
||||
"@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||||
"#{inspect(fields)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -257,7 +266,7 @@ defmodule Kernel.Utils do
|
||||
module.exception([])
|
||||
end
|
||||
|
||||
def raise(%_{__exception__: _} = exception) do
|
||||
def raise(%_{__exception__: true} = exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
@@ -320,27 +329,21 @@ defmodule Kernel.Utils do
|
||||
|
||||
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
|
||||
def defguard(args, expr, env) do
|
||||
{_, vars} = extract_refs_from_args(args)
|
||||
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars)
|
||||
body_expr = expand_defguard(expr, %{env | context: nil}, vars)
|
||||
{^args, vars} = extract_refs_from_args(args)
|
||||
env = :elixir_env.with_vars(%{env | context: :guard}, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
|
||||
quote do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
unquote(literal_quote(unquote_every_ref(guard_expr, vars), []))
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true))
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_defguard(expr, env, vars) do
|
||||
env = :elixir_env.with_vars(env, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
expr
|
||||
end
|
||||
|
||||
defp extract_refs_from_args(args) do
|
||||
Macro.postwalk(args, [], fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
@@ -366,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) ->
|
||||
@@ -379,7 +382,7 @@ defmodule Kernel.Utils do
|
||||
{new_var, acc}
|
||||
|
||||
%{} ->
|
||||
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
new_var = Macro.unique_var(generated, module)
|
||||
{new_var, Map.put(acc, pair, {new_var, var})}
|
||||
end
|
||||
|
||||
+44
-77
@@ -38,7 +38,7 @@ defmodule Keyword do
|
||||
|
||||
## Duplicate keys and ordering
|
||||
|
||||
A keyword list may have duplicate keys so it is not strictly a key-value
|
||||
A keyword may have duplicate keys so it is not strictly a key-value
|
||||
data type. However, most of the functions in this module work on a
|
||||
key-value structure and behave similar to the functions you would
|
||||
find in the `Map` module. For example, `Keyword.get/3` will get the first
|
||||
@@ -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 """
|
||||
@@ -269,60 +260,38 @@ defmodule Keyword do
|
||||
@doc since: "1.13.0"
|
||||
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
|
||||
{:ok, keyword()} | {:error, [atom]}
|
||||
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
|
||||
|
||||
def validate(keyword, values) when is_list(keyword) and is_list(values) do
|
||||
validate_merge(keyword, values, [], keyword)
|
||||
validate(keyword, values, [], [], [])
|
||||
end
|
||||
|
||||
defp validate_merge([], values, values_pre, original),
|
||||
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))}
|
||||
|
||||
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original)
|
||||
when is_atom(key) do
|
||||
case head do
|
||||
^key -> validate_merge(keyword, tail, values_pre, original)
|
||||
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
|
||||
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
|
||||
case find_key!(key, values_pre, []) do
|
||||
{new_values, new_values_pre} ->
|
||||
validate_merge(keyword, new_values, new_values_pre, original)
|
||||
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, [pair | acc], bad_keys)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values_pre, [key])
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, acc, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_merge([pair | _], _, _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
)
|
||||
|
||||
defp validate_fallback([{key, _} | keyword], values, bad_keys)
|
||||
when is_atom(key) do
|
||||
case find_key!(key, values, []) do
|
||||
{rest, acc} ->
|
||||
validate_fallback(keyword, rest ++ acc, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values, [key | bad_keys])
|
||||
end
|
||||
defp validate([], values1, values2, acc, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
|
||||
end
|
||||
|
||||
defp validate_fallback([], _, bad), do: {:error, bad}
|
||||
defp validate([], _values1, _values2, _acc, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
end
|
||||
|
||||
defp validate_fallback([p | _], _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
|
||||
)
|
||||
defp validate([pair | _], _values1, _values2, _acc, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
|
||||
defp find_key!(key, [key | rest], acc), do: {rest, acc}
|
||||
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
|
||||
@@ -468,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
|
||||
@@ -514,10 +483,10 @@ 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, delete(t, key))}
|
||||
{current, :lists.reverse(acc, t)}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
@@ -540,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
|
||||
@@ -577,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, delete(t, key))}
|
||||
{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
|
||||
@@ -987,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])
|
||||
@@ -1323,8 +1291,7 @@ defmodule Keyword do
|
||||
@doc """
|
||||
Drops the given `keys` from the keyword list.
|
||||
|
||||
If a key occurs multiple times, all of its entries are removed when that key
|
||||
is included in `keys`.
|
||||
Removes duplicate keys from the new keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1365,9 +1332,9 @@ defmodule Keyword do
|
||||
"""
|
||||
@spec pop(t, key, default) :: {value | default, t}
|
||||
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {default, keywords}
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {default, keywords}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1392,9 +1359,9 @@ defmodule Keyword do
|
||||
@doc since: "1.10.0"
|
||||
@spec pop!(t, key) :: {value, t}
|
||||
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> raise KeyError, key: key, term: keywords
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> raise KeyError, key: key, term: keywords
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1434,7 +1401,7 @@ defmodule Keyword do
|
||||
do: {values, acc}
|
||||
|
||||
@doc """
|
||||
Lazily returns the first value for `key` and removes all associated entries in the keyword list.
|
||||
Lazily returns and removes all values associated with `key` in the keyword list.
|
||||
|
||||
This is useful if the default value is very expensive to calculate or
|
||||
generally difficult to set up and tear down again.
|
||||
@@ -1457,9 +1424,9 @@ defmodule Keyword do
|
||||
@spec pop_lazy(t, key, (-> value)) :: {value, t}
|
||||
def pop_lazy(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {fun.(), keywords}
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {fun.(), keywords}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+28
-143
@@ -102,7 +102,7 @@ defmodule List do
|
||||
Even though the representation changed, the raw data does remain a list of
|
||||
integers, which can be handled as such:
|
||||
|
||||
iex> inspect(~c"abc", charlists: :as_lists)
|
||||
iex> inspect(~c"abc", charlists: :as_list)
|
||||
"[97, 98, 99]"
|
||||
iex> Enum.map(~c"abc", fn num -> 1000 + num end)
|
||||
[1097, 1098, 1099]
|
||||
@@ -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,30 +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
|
||||
|
||||
"""
|
||||
@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.
|
||||
|
||||
@@ -350,31 +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
|
||||
|
||||
"""
|
||||
@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
|
||||
@@ -429,7 +379,7 @@ defmodule List do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec keyfind!([tuple], any, non_neg_integer) :: tuple
|
||||
@spec keyfind!([tuple], any, non_neg_integer) :: any
|
||||
def keyfind!(list, key, position) when is_integer(position) do
|
||||
:lists.keyfind(key, position + 1, list) ||
|
||||
raise KeyError,
|
||||
@@ -514,8 +464,8 @@ defmodule List do
|
||||
|
||||
As in `Enum.sort/2`, avoid using the default sorting function to sort
|
||||
structs, as by default it performs structural comparison instead of a
|
||||
semantic one. In such cases, you shall pass a sorting function as the third
|
||||
argument or any module that implements a `compare/2` function. For example,
|
||||
semantic one. In such cases, you shall pass a sorting function as third
|
||||
element or any module that implements a `compare/2` function. For example,
|
||||
if you have tuples with user names and their birthday, and you want to
|
||||
sort on their birthday, in both ascending and descending order, you should
|
||||
do:
|
||||
@@ -655,7 +605,7 @@ defmodule List do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps `term` in a list if it is not a list.
|
||||
Wraps `term` in a list if this is not list.
|
||||
|
||||
If `term` is already a list, it returns the list.
|
||||
If `term` is `nil`, it returns an empty list.
|
||||
@@ -954,12 +904,9 @@ defmodule List do
|
||||
@spec pop_at(list, integer, any) :: {any, list}
|
||||
def pop_at(list, index, default \\ nil) when is_integer(index) do
|
||||
if index < 0 do
|
||||
case length(list) + index do
|
||||
index when index < 0 -> {default, list}
|
||||
index -> do_pop_at(list, index, default, [], list)
|
||||
end
|
||||
do_pop_at(list, length(list) + index, default, [])
|
||||
else
|
||||
do_pop_at(list, index, default, [], list)
|
||||
do_pop_at(list, index, default, [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1021,51 +968,31 @@ defmodule List do
|
||||
:lists.suffix(suffix, list)
|
||||
end
|
||||
|
||||
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
|
||||
def to_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom or creates a new one.
|
||||
Converts a charlist to an atom.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point.
|
||||
|
||||
> #### Dynamic Atom Creation {: .warning}
|
||||
>
|
||||
> This function creates atoms dynamically and atoms are
|
||||
> not garbage-collected. Therefore, `charlist` should not be an
|
||||
> untrusted value, such as input received from a socket or during
|
||||
> a web request. Consider using `to_existing_atom/1` instead.
|
||||
|
||||
By default, the maximum number of atoms is `1_048_576`. This limit
|
||||
can be raised or lowered using the VM option `+t`.
|
||||
|
||||
The maximum atom size is 255 Unicode code points.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_unsafe_atom(~c"Elixir")
|
||||
iex> List.to_atom(~c"Elixir")
|
||||
:Elixir
|
||||
|
||||
iex> List.to_unsafe_atom(~c"🌢 Elixir")
|
||||
iex> List.to_atom(~c"🌢 Elixir")
|
||||
:"🌢 Elixir"
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_unsafe_atom(charlist) :: atom
|
||||
def to_unsafe_atom(charlist) do
|
||||
@spec to_atom(charlist) :: atom
|
||||
def to_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom.
|
||||
|
||||
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point. Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
@@ -1096,47 +1023,6 @@ defmodule List do
|
||||
:erlang.list_to_existing_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to one of the `allowed_atoms` or raises.
|
||||
|
||||
Raises an `ArgumentError` if the atom either does not exist or is not within
|
||||
the existing list.
|
||||
|
||||
This should be preferred to `to_existing_atom/1` if the list is known upfront,
|
||||
since there is no risk that the atom has not been loaded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
|
||||
:foo
|
||||
|
||||
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
|
||||
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
|
||||
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
|
||||
atom = :erlang.list_to_existing_atom(charlist)
|
||||
|
||||
if atom not in allowed_atoms do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
atom
|
||||
end
|
||||
|
||||
# used just to have a less cryptic stacktrace and consistent error
|
||||
@doc false
|
||||
def __to_existing_atom__(charlist, allowed_atoms) do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
|
||||
raise ArgumentError,
|
||||
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the float whose text representation is `charlist`.
|
||||
|
||||
@@ -1148,7 +1034,7 @@ defmodule List do
|
||||
2.2017764
|
||||
|
||||
"""
|
||||
@spec to_float(nonempty_charlist) :: float
|
||||
@spec to_float(charlist) :: float
|
||||
def to_float(charlist) do
|
||||
:erlang.list_to_float(charlist)
|
||||
end
|
||||
@@ -1164,7 +1050,7 @@ defmodule List do
|
||||
123
|
||||
|
||||
"""
|
||||
@spec to_integer(nonempty_charlist) :: integer
|
||||
@spec to_integer(charlist) :: integer
|
||||
def to_integer(charlist) do
|
||||
:erlang.list_to_integer(charlist)
|
||||
end
|
||||
@@ -1182,7 +1068,7 @@ defmodule List do
|
||||
1023
|
||||
|
||||
"""
|
||||
@spec to_integer(nonempty_charlist, 2..36) :: integer
|
||||
@spec to_integer(charlist, 2..36) :: integer
|
||||
def to_integer(charlist, base) do
|
||||
:erlang.list_to_integer(charlist, base)
|
||||
end
|
||||
@@ -1251,7 +1137,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
#{inspect(list)}
|
||||
"""
|
||||
else
|
||||
result when is_binary(result) ->
|
||||
@@ -1303,7 +1189,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
#{inspect(list)}
|
||||
"""
|
||||
else
|
||||
result when is_list(result) ->
|
||||
@@ -1368,13 +1254,13 @@ defmodule List do
|
||||
|
||||
defp myers_difference_with_diff_script(list1, list2, diff_script) do
|
||||
path = {0, list1, list2, []}
|
||||
find_script(0, [path], diff_script)
|
||||
find_script(0, length(list1) + length(list2), [path], diff_script)
|
||||
end
|
||||
|
||||
defp find_script(envelope, paths, diff_script) do
|
||||
defp find_script(envelope, max, paths, diff_script) do
|
||||
case each_diagonal(-envelope, envelope, paths, [], diff_script) do
|
||||
{:done, edits} -> compact_reverse(edits, [])
|
||||
{:next, paths} -> find_script(envelope + 1, paths, diff_script)
|
||||
{:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1519,16 +1405,15 @@ defmodule List do
|
||||
|
||||
# pop_at
|
||||
|
||||
# The original list is returned when the index is out of bounds
|
||||
defp do_pop_at([], _index, default, _acc, original) do
|
||||
{default, original}
|
||||
defp do_pop_at([], _index, default, acc) do
|
||||
{default, :lists.reverse(acc)}
|
||||
end
|
||||
|
||||
defp do_pop_at([head | tail], 0, _default, acc, _original) do
|
||||
defp do_pop_at([head | tail], 0, _default, acc) do
|
||||
{head, :lists.reverse(acc, tail)}
|
||||
end
|
||||
|
||||
defp do_pop_at([head | tail], index, default, acc, original) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc], original)
|
||||
defp do_pop_at([head | tail], index, default, acc) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
+100
-193
@@ -166,7 +166,7 @@ defmodule Macro do
|
||||
of a `__block__` or the right side of `->`. The last expression of the
|
||||
block does not have metadata if it is not followed by an end of line
|
||||
character (either a newline or `;`). This entry may appear multiple times
|
||||
in the same metadata if the expression is surrounded by parens
|
||||
in the same metadata if the expression is surround by parens
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword.
|
||||
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
|
||||
@@ -235,7 +235,7 @@ defmodule Macro do
|
||||
`div/2` function, so that the AST for that function will become `{:div, [],
|
||||
[100, 5]}` (`div(100, 5)`).
|
||||
"""
|
||||
@spec unpipe(t()) :: [{t(), non_neg_integer}]
|
||||
@spec unpipe(t()) :: [t()]
|
||||
def unpipe(expr) do
|
||||
:lists.reverse(unpipe(expr, []))
|
||||
end
|
||||
@@ -508,17 +508,19 @@ 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"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) ::
|
||||
[{atom, metadata(), context}, ...]
|
||||
[{atom, [counter: integer], context}, ...]
|
||||
when context: atom
|
||||
def generate_unique_arguments(amount, context),
|
||||
do: generate_arguments(amount, context, &unique_var/2)
|
||||
@@ -527,7 +529,7 @@ defmodule Macro do
|
||||
|
||||
defp generate_arguments(amount, context, fun)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context)
|
||||
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -568,15 +570,15 @@ 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"
|
||||
@spec unique_var(var, context) :: {var, metadata(), context}
|
||||
@spec unique_var(var, context) :: {var, [counter: integer], context}
|
||||
when var: atom, context: atom
|
||||
def unique_var(var, context) when is_atom(var) and is_atom(context) do
|
||||
{var, [counter: :elixir_module.next_counter(context)], context}
|
||||
@@ -690,39 +692,7 @@ defmodule Macro do
|
||||
"""
|
||||
@spec prewalk(t, (t -> t)) :: t
|
||||
def prewalk(ast, fun) when is_function(fun, 1) do
|
||||
do_prewalk(fun.(ast), fun)
|
||||
end
|
||||
|
||||
# Mirrors do_traverse/4 with an always-pre fun and no accumulator,
|
||||
# avoiding the wrapper closures and tuple threading of traverse/4.
|
||||
# Each clause dispatches on the already-transformed node, so fun's
|
||||
# rewrites are descended into, exactly as in traverse/4.
|
||||
defp do_prewalk({form, meta, args}, fun) when is_atom(form) do
|
||||
{form, meta, do_prewalk_args(args, fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk({form, meta, args}, fun) do
|
||||
form = do_prewalk(fun.(form), fun)
|
||||
{form, meta, do_prewalk_args(args, fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk({left, right}, fun) do
|
||||
left = do_prewalk(fun.(left), fun)
|
||||
{left, do_prewalk(fun.(right), fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk(list, fun) when is_list(list) do
|
||||
do_prewalk_args(list, fun)
|
||||
end
|
||||
|
||||
defp do_prewalk(x, _fun) do
|
||||
x
|
||||
end
|
||||
|
||||
defp do_prewalk_args(args, _fun) when is_atom(args), do: args
|
||||
|
||||
defp do_prewalk_args(args, fun) when is_list(args) do
|
||||
:lists.map(fn x -> do_prewalk(fun.(x), fun) end, args)
|
||||
elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -760,41 +730,11 @@ defmodule Macro do
|
||||
"""
|
||||
@spec postwalk(t, (t -> t)) :: t
|
||||
def postwalk(ast, fun) when is_function(fun, 1) do
|
||||
do_postwalk(ast, fun)
|
||||
end
|
||||
|
||||
# Mirrors do_traverse/4 with an always-post fun and no accumulator,
|
||||
# avoiding the wrapper closures and tuple threading of traverse/4
|
||||
defp do_postwalk({form, meta, args}, fun) when is_atom(form) do
|
||||
fun.({form, meta, do_postwalk_args(args, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk({form, meta, args}, fun) do
|
||||
form = do_postwalk(form, fun)
|
||||
fun.({form, meta, do_postwalk_args(args, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk({left, right}, fun) do
|
||||
left = do_postwalk(left, fun)
|
||||
fun.({left, do_postwalk(right, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk(list, fun) when is_list(list) do
|
||||
fun.(do_postwalk_args(list, fun))
|
||||
end
|
||||
|
||||
defp do_postwalk(x, fun) do
|
||||
fun.(x)
|
||||
end
|
||||
|
||||
defp do_postwalk_args(args, _fun) when is_atom(args), do: args
|
||||
|
||||
defp do_postwalk_args(args, fun) when is_list(args) do
|
||||
:lists.map(fn x -> do_postwalk(x, fun) end, args)
|
||||
elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function behaves like `prewalk/3`, but performs a depth-first,
|
||||
This functions behaves like `prewalk/3`, but performs a depth-first,
|
||||
post-order traversal of quoted expressions using an accumulator.
|
||||
"""
|
||||
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
|
||||
@@ -1002,7 +942,7 @@ defmodule Macro do
|
||||
|
||||
This is useful when a struct needs to be expanded at
|
||||
compilation time and the struct being expanded may or may
|
||||
not have been compiled (including structs defined
|
||||
not have been compiled (including structs in the defined
|
||||
under the module being compiled). For compiled modules,
|
||||
it will invoke `module.__info__(:struct)`.
|
||||
|
||||
@@ -1019,9 +959,8 @@ defmodule Macro do
|
||||
backwards compatibility purposes.
|
||||
|
||||
In future releases, Elixir may introduce truly required struct
|
||||
fields, the required field will be removed and default will be
|
||||
present only if the field is optional. Your code should prepare
|
||||
for such scenario accordingly.
|
||||
fields, and therefore only one of required or default will be
|
||||
present. Your code should prepare for such scenario accordingly.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec struct_info!(module(), Macro.Env.t()) ::
|
||||
@@ -1033,15 +972,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
|
||||
|
||||
@@ -1096,7 +1029,7 @@ defmodule Macro do
|
||||
defp find_invalid(other), do: {:error, other}
|
||||
|
||||
@doc """
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
pre-order traversal.
|
||||
|
||||
## Examples
|
||||
@@ -1154,7 +1087,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
post-order traversal.
|
||||
|
||||
## Examples
|
||||
@@ -1257,15 +1190,14 @@ defmodule Macro do
|
||||
The mapping function receives an integer representing the code point
|
||||
of the character it wants to unescape. There are also the special atoms
|
||||
`:newline`, `:unicode`, and `:hex`, which control newline, unicode,
|
||||
and escaping respectively, and for which the mapping function must return
|
||||
a boolean.
|
||||
and escaping respectively.
|
||||
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(:newline), do: true
|
||||
def unescape_map(:unicode), do: true
|
||||
def unescape_map(:hex), do: true
|
||||
def unescape_map(?0), do: 0
|
||||
def unescape_map(?0), do: ?0
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(?b), do: ?\b
|
||||
def unescape_map(?d), do: ?\d
|
||||
@@ -1288,11 +1220,7 @@ defmodule Macro do
|
||||
Macro.unescape_string("example\\n", &unescape_map(&1))
|
||||
|
||||
"""
|
||||
@spec unescape_string(
|
||||
String.t(),
|
||||
(non_neg_integer | :newline | :unicode | :hex ->
|
||||
non_neg_integer | boolean)
|
||||
) :: String.t()
|
||||
@spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
|
||||
def unescape_string(string, map) do
|
||||
:elixir_interpolation.unescape_string(string, map)
|
||||
end
|
||||
@@ -1807,9 +1735,9 @@ defmodule Macro do
|
||||
|
||||
defp kw_blocks_to_string(kw, fun) do
|
||||
Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
|
||||
case Keyword.fetch(kw, x) do
|
||||
{:ok, value} -> acc <> kw_block_to_string(x, value, fun)
|
||||
:error -> acc
|
||||
case Keyword.has_key?(kw, x) do
|
||||
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
|
||||
false -> acc
|
||||
end
|
||||
end) <> "end"
|
||||
end
|
||||
@@ -1923,7 +1851,6 @@ defmodule Macro do
|
||||
definition compile-time, and you can use `Macro.expand/2`.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec compile_apply(module(), atom(), [term()], Macro.Env.t()) :: term()
|
||||
def compile_apply(mod, fun, args, caller) do
|
||||
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
|
||||
Kernel.apply(mod, fun, args)
|
||||
@@ -2048,9 +1975,10 @@ defmodule Macro do
|
||||
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
|
||||
when is_atom(atom) and is_atom(field) and env.context != :match do
|
||||
case Map.fetch(env, field) do
|
||||
{:ok, value} -> {maybe_escape_map(value), true}
|
||||
:error -> {original, false}
|
||||
if Map.has_key?(env, field) do
|
||||
{maybe_escape_map(Map.get(env, field)), true}
|
||||
else
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2188,9 +2116,6 @@ defmodule Macro do
|
||||
def quoted_literal?({:__aliases__, _, args}),
|
||||
do: quoted_literal?(args)
|
||||
|
||||
def quoted_literal?({:__block__, _, [wrapped]}),
|
||||
do: quoted_literal?(wrapped)
|
||||
|
||||
def quoted_literal?({:%, _, [left, right]}),
|
||||
do: quoted_literal?(left) and quoted_literal?(right)
|
||||
|
||||
@@ -2266,7 +2191,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
|
||||
@@ -2570,7 +2495,7 @@ defmodule Macro do
|
||||
|
||||
### As a remote call
|
||||
|
||||
Inspect an atom as the function name of a remote call.
|
||||
Inspect an atom the function name of a remote call.
|
||||
|
||||
iex> Macro.inspect_atom(:remote_call, :foo)
|
||||
"foo"
|
||||
@@ -2691,76 +2616,54 @@ defmodule Macro do
|
||||
:unquoted_operator
|
||||
|
||||
true ->
|
||||
classify_binary(Atom.to_string(atom), atom)
|
||||
end
|
||||
end
|
||||
charlist = Atom.to_charlist(atom)
|
||||
|
||||
# ASCII identifiers and aliases are recognized on the binary to avoid building a
|
||||
# charlist and running the (unicode aware) tokenizer, which dominates the cost of
|
||||
# classifying keyword list, map and struct keys.
|
||||
defp classify_binary(<<char, rest::binary>>, atom)
|
||||
when char >= ?a and char <= ?z
|
||||
when char == ?_ do
|
||||
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom)
|
||||
end
|
||||
if valid_alias?(charlist) do
|
||||
:alias
|
||||
else
|
||||
case :elixir_config.identifier_tokenizer().tokenize(charlist) do
|
||||
{kind, _acc, [], _, _, special} ->
|
||||
cond do
|
||||
kind != :identifier or :lists.member(:at, special) ->
|
||||
:not_callable
|
||||
|
||||
defp classify_binary("Elixir" <> rest, atom) do
|
||||
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom)
|
||||
end
|
||||
# identifier_tokenizer used to return errors for non-nfc, but
|
||||
# now it nfc-normalizes everything. However, lack of nfc is
|
||||
# still a good reason to quote an atom when printing.
|
||||
:lists.member(:nfkc, special) ->
|
||||
:other
|
||||
|
||||
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom)
|
||||
true ->
|
||||
:identifier
|
||||
end
|
||||
|
||||
defp classify_with_tokenizer(atom) do
|
||||
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do
|
||||
{kind, _acc, [], _, _, special} ->
|
||||
cond do
|
||||
kind != :identifier or :lists.member(:at, special) ->
|
||||
:not_callable
|
||||
|
||||
# identifier_tokenizer used to return errors for non-nfc, but
|
||||
# now it nfc-normalizes everything. However, lack of nfc is
|
||||
# still a good reason to quote an atom when printing.
|
||||
:lists.member(:nfkc, special) ->
|
||||
:other
|
||||
|
||||
true ->
|
||||
:identifier
|
||||
_ ->
|
||||
:other
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
:other
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_identifier_rest?(<<char, rest::binary>>)
|
||||
when char >= ?a and char <= ?z
|
||||
when char >= ?A and char <= ?Z
|
||||
when char >= ?0 and char <= ?9
|
||||
when char == ?_ do
|
||||
valid_identifier_rest?(rest)
|
||||
end
|
||||
defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
|
||||
defp valid_alias?(_other), do: false
|
||||
|
||||
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true
|
||||
defp valid_identifier_rest?(<<>>), do: true
|
||||
defp valid_identifier_rest?(_other), do: false
|
||||
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
|
||||
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
|
||||
|
||||
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z,
|
||||
do: valid_alias_piece_rest?(rest)
|
||||
|
||||
defp valid_alias_piece?(<<>>), do: true
|
||||
defp valid_alias_piece?([]), do: true
|
||||
defp valid_alias_piece?(_other), do: false
|
||||
|
||||
# A helper returning the rest of the binary would build a sub binary per piece,
|
||||
# so branch back into valid_alias_piece?/1 to keep the match context.
|
||||
defp valid_alias_piece_rest?(<<char, rest::binary>>)
|
||||
defp trim_leading_while_valid_identifier([char | rest])
|
||||
when char >= ?a and char <= ?z
|
||||
when char >= ?A and char <= ?Z
|
||||
when char >= ?0 and char <= ?9
|
||||
when char == ?_ do
|
||||
valid_alias_piece_rest?(rest)
|
||||
trim_leading_while_valid_identifier(rest)
|
||||
end
|
||||
|
||||
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other)
|
||||
defp trim_leading_while_valid_identifier(other) do
|
||||
other
|
||||
end
|
||||
|
||||
@doc """
|
||||
Default backend for `Kernel.dbg/2`.
|
||||
@@ -2791,13 +2694,13 @@ defmodule Macro do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, dbg is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
prelude = quote do: options = unquote(options)
|
||||
prelude = quote do: options = unquote(Macro.escape(options))
|
||||
acc = {prelude, dbg_format_header(env)}
|
||||
|
||||
{acc, nil} =
|
||||
@@ -2818,29 +2721,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]
|
||||
@@ -3046,8 +2954,6 @@ defmodule Macro do
|
||||
@doc false
|
||||
def __dbg__(to_debug, header, options) do
|
||||
{print_location?, options} = Keyword.pop(options, :print_location, true)
|
||||
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
||||
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
||||
ansi_enabled? = options[:syntax_colors] != []
|
||||
|
||||
if print_location? and is_binary(header) do
|
||||
@@ -3055,24 +2961,25 @@ defmodule Macro do
|
||||
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
|
||||
end
|
||||
|
||||
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
||||
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
||||
{formatted, result} = dbg_format_ast_to_debug(to_debug, options)
|
||||
:ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?))
|
||||
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
|
||||
|
||||
+15
-16
@@ -203,23 +203,23 @@ defmodule Macro.Env do
|
||||
Returns a keyword list containing the file and line
|
||||
information as keys.
|
||||
"""
|
||||
@spec location(t) :: [file: file, line: line]
|
||||
@spec location(t) :: keyword
|
||||
def location(env)
|
||||
|
||||
def location(%{__struct__: Macro.Env, file: file, line: line}) do
|
||||
[file: file, line: line]
|
||||
end
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
|
||||
when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
|
||||
@doc """
|
||||
Returns the modules from which the given `{name, arity}` was
|
||||
@@ -234,7 +234,7 @@ defmodule Macro.Env do
|
||||
> This function does not emit compiler tracing events,
|
||||
> which may block the compiler from correctly tracking
|
||||
> dependencies. Use this function for reflection purposes
|
||||
> but do not use it to expand imports into qualified
|
||||
> but to do not use it to expand imports into qualified
|
||||
> calls. Instead, use `expand_import/5`.
|
||||
|
||||
## Examples
|
||||
@@ -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
|
||||
|
||||
@@ -345,7 +345,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForms.require/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
@@ -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)
|
||||
@@ -386,7 +385,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForms.import/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
|
||||
|
||||
* `:emit_warnings` - emit warnings found when defining imports
|
||||
|
||||
@@ -394,7 +393,7 @@ defmodule Macro.Env do
|
||||
|
||||
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
|
||||
The function will be invoked with `:functions` or `:macros` argument.
|
||||
It has to return a list of `{function, arity}` key-value pairs.
|
||||
It has to return a list of `{function, arity}` key value pairs.
|
||||
If it fails, it defaults to using module metadata based on `module_info/1`.
|
||||
|
||||
## Examples
|
||||
@@ -406,7 +405,7 @@ defmodule Macro.Env do
|
||||
iex> Macro.Env.lookup_import(env, {:flatten, 1})
|
||||
[{:function, List}]
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForms.import/2`:
|
||||
It accepts the same options as `Kernel.SpecialForm.import/2`:
|
||||
|
||||
iex> env = __ENV__
|
||||
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
|
||||
@@ -448,7 +447,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForms.alias/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
@@ -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
|
||||
@@ -700,7 +699,7 @@ defmodule Macro.Env do
|
||||
@doc """
|
||||
Returns the environment stacktrace.
|
||||
"""
|
||||
@spec stacktrace(t) :: [{module, atom, arity, keyword}]
|
||||
@spec stacktrace(t) :: list
|
||||
def stacktrace(%{__struct__: Macro.Env} = env) do
|
||||
cond do
|
||||
is_nil(env.module) ->
|
||||
|
||||
+55
-106
@@ -201,7 +201,7 @@ defmodule Map do
|
||||
%{}
|
||||
|
||||
"""
|
||||
@spec new :: %{}
|
||||
@spec new :: map
|
||||
def new, do: %{}
|
||||
|
||||
@doc """
|
||||
@@ -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)
|
||||
|
||||
+18
-27
@@ -51,24 +51,11 @@ defmodule MapSet do
|
||||
that they share many properties, including logarithmic time complexity. Erlang
|
||||
`:sets` (version 2) are implemented on top of maps, so see the documentation
|
||||
for `Map` for more information on its execution time complexity.
|
||||
|
||||
> #### Dialyzer opaqueness warnings {: .warning}
|
||||
>
|
||||
> `MapSet` internally relies on the `:sets` module which uses
|
||||
> opaque types. This might cause Dialyzer to report opaqueness violations.
|
||||
> These can be silenced by setting the following module attribute:
|
||||
>
|
||||
> @dialyzer :no_opaque
|
||||
|
||||
"""
|
||||
|
||||
@type value :: term
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -180,14 +167,18 @@ defmodule MapSet do
|
||||
@doc since: "1.14.0"
|
||||
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
|
||||
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
|
||||
map =
|
||||
if :sets.is_disjoint(set1, set2) do
|
||||
:sets.union(set1, set2)
|
||||
else
|
||||
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
|
||||
end
|
||||
{small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
|
||||
|
||||
%{map_set1 | map: map}
|
||||
disjointer_fun = fn elem, {small, acc} ->
|
||||
if :sets.is_element(elem, small) do
|
||||
{:sets.del_element(elem, small), acc}
|
||||
else
|
||||
{small, [elem | acc]}
|
||||
end
|
||||
end
|
||||
|
||||
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
|
||||
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -210,7 +201,7 @@ defmodule MapSet do
|
||||
Checks if two sets are equal.
|
||||
|
||||
The comparison between elements is done using `===/2`,
|
||||
which means a set with `1` is not equivalent to a set with
|
||||
which a set with `1` is not equivalent to a set with
|
||||
`1.0`.
|
||||
|
||||
## Examples
|
||||
@@ -349,7 +340,7 @@ defmodule MapSet do
|
||||
> If you find yourself doing multiple calls to `MapSet.filter/2`
|
||||
> and `MapSet.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
|
||||
> a set at the end using `MapSet.new/1`.
|
||||
> a map at the end using `MapSet.new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -405,15 +396,15 @@ defmodule MapSet do
|
||||
iex> while_false
|
||||
MapSet.new([1, 3])
|
||||
|
||||
iex> {while_true, while_false} = MapSet.split_with(MapSet.new([10, 20, 60, 70]), fn v -> v > 50 end)
|
||||
iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end)
|
||||
iex> while_true
|
||||
MapSet.new([60, 70])
|
||||
MapSet.new([])
|
||||
iex> while_false
|
||||
MapSet.new([10, 20])
|
||||
MapSet.new([])
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec split_with(t(a), (a -> as_boolean(term))) :: {t(a), t(a)} when a: value
|
||||
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
|
||||
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
|
||||
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
|
||||
{%MapSet{map: while_true}, %MapSet{map: while_false}}
|
||||
|
||||
+64
-67
@@ -190,7 +190,7 @@ defmodule Module do
|
||||
|
||||
* a string (often a heredoc)
|
||||
* `false`, which will make the entity invisible to documentation-extraction
|
||||
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/)
|
||||
tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
|
||||
* a keyword list, since Elixir 1.7.0
|
||||
|
||||
For example:
|
||||
@@ -216,7 +216,7 @@ defmodule Module do
|
||||
|
||||
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
|
||||
also accept a keyword list that serves as a way to provide arbitrary metadata
|
||||
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and
|
||||
about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
|
||||
`IEx` may use this information to display annotations. A common use
|
||||
case is the `:since` key, which may be used to annotate in which version the
|
||||
function was introduced.
|
||||
@@ -263,7 +263,7 @@ defmodule Module do
|
||||
|
||||
Tools may use this information to ensure the module is recompiled
|
||||
in case any of the external resources change, see for example:
|
||||
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html).
|
||||
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
|
||||
|
||||
The specified file path provided is interpreted as relative to
|
||||
the folder containing the project's `mix.exs`, which is the
|
||||
@@ -321,7 +321,7 @@ defmodule Module do
|
||||
|
||||
Accepts a string (often a heredoc) or `false` where `@moduledoc false`
|
||||
will make the module invisible to documentation extraction tools like
|
||||
[`ExDoc`](https://ex-doc.hexdocs.pm/).
|
||||
[`ExDoc`](https://hexdocs.pm/ex_doc/).
|
||||
|
||||
Similarly to `@doc` also accepts a keyword list to provide metadata
|
||||
about the module. For more details, see the documentation of `@doc`
|
||||
@@ -366,8 +366,7 @@ defmodule Module do
|
||||
Unlike other hooks, `@on_definition` will only invoke functions and
|
||||
never macros. This is to avoid `@on_definition` callbacks from
|
||||
redefining functions that have just been defined in favor of more
|
||||
explicit approaches. They are also invoked in the reverse order of
|
||||
registration.
|
||||
explicit approaches.
|
||||
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__on_definition__/6`.
|
||||
@@ -403,21 +402,30 @@ defmodule Module do
|
||||
|
||||
Accepts the function name (as an atom) of a function in the current module.
|
||||
The function must have an arity of 0 (no arguments). If the function does
|
||||
not return `:ok`, the loading of the module will be aborted. Its primary
|
||||
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
|
||||
not return `:ok`, the loading of the module will be aborted.
|
||||
For example:
|
||||
|
||||
defmodule MyModule do
|
||||
@on_load :load_external_code
|
||||
@on_load :load_check
|
||||
|
||||
def load_external_code do
|
||||
:erlang.load_nif(~c"path/to/extension.so_or_dll")
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
:abort
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
The function given to `on_load` should avoid calling functions from
|
||||
other modules. This is because, when running a `mix release`,
|
||||
`on_load` runs extremely early, before any application starts running,
|
||||
and therefore even systems like the `Logger` and `IO` are not yet available.
|
||||
other modules. If you must call functions in other modules and those
|
||||
modules are defined within the same project, the called modules must
|
||||
have the `@compile {:autoload, true}` annotation, so they are loaded
|
||||
upfront (and not from within the `@on_load` callback).
|
||||
|
||||
### `@vsn`
|
||||
|
||||
@@ -557,7 +565,7 @@ defmodule Module do
|
||||
written to disk, and therefore raising does not effectively halt compilation
|
||||
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
|
||||
or other callback. Given modules have already been compiled, functions in
|
||||
this module, such as `get_attribute/2`, which expect modules to not have been
|
||||
ths module, such as `get_attribute/2`, which expect modules to not have been
|
||||
yet compiled, do not work on `@after_verify` callback.
|
||||
|
||||
Accepts a module or a `{module, function_name}` tuple. The function
|
||||
@@ -657,8 +665,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]
|
||||
@@ -681,7 +688,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 :: [
|
||||
@@ -754,7 +761,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec reserved_attributes() :: %{optional(atom()) => %{doc: binary()}}
|
||||
@spec reserved_attributes() :: map
|
||||
def reserved_attributes() do
|
||||
%{
|
||||
after_compile: %{
|
||||
@@ -960,7 +967,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
|
||||
@@ -986,7 +993,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
|
||||
@@ -1017,7 +1024,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
|
||||
@@ -1041,7 +1048,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
|
||||
@@ -1148,7 +1155,7 @@ defmodule Module do
|
||||
defp simplify_var(var, guess_priority) do
|
||||
case Atom.to_string(var) do
|
||||
"_" -> {:_, [], guess_priority}
|
||||
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority}
|
||||
"_" <> rest -> {String.to_atom(rest), [], guess_priority}
|
||||
_ -> {var, [], nil}
|
||||
end
|
||||
end
|
||||
@@ -1159,7 +1166,7 @@ defmodule Module do
|
||||
rescue
|
||||
ArgumentError -> module
|
||||
else
|
||||
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name)))
|
||||
module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1192,7 +1199,7 @@ defmodule Module do
|
||||
defp expand_key(key, counters) do
|
||||
case counters do
|
||||
%{^key => count} when is_integer(count) and count >= 1 ->
|
||||
{{String.to_unsafe_atom("#{key}#{count}"), [], Elixir}, Map.put(counters, key, count - 1)}
|
||||
{{:"#{key}#{count}", [], Elixir}, Map.put(counters, key, count - 1)}
|
||||
|
||||
_ ->
|
||||
{{key, [], Elixir}, counters}
|
||||
@@ -1225,8 +1232,7 @@ defmodule Module do
|
||||
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
|
||||
|
||||
# Otherwise, returns a generic guess
|
||||
defp merge_signature({_, meta, _}, _newer, i),
|
||||
do: {String.to_unsafe_atom("arg#{i}"), meta, Elixir}
|
||||
defp merge_signature({_, meta, _}, _newer, i), do: {:"arg#{i}", meta, Elixir}
|
||||
|
||||
@doc """
|
||||
Checks if the module defines the given function or macro.
|
||||
@@ -1252,18 +1258,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
|
||||
@@ -1279,13 +1284,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
|
||||
@@ -1298,9 +1302,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
|
||||
|
||||
@@ -1313,9 +1315,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
|
||||
|
||||
@@ -1348,7 +1348,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.
|
||||
@@ -1367,7 +1367,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)
|
||||
@@ -1416,10 +1416,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 """
|
||||
@@ -1450,17 +1450,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}
|
||||
|
||||
@@ -1477,11 +1476,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 """
|
||||
@@ -1497,20 +1495,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 ->
|
||||
@@ -1519,7 +1517,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
|
||||
@@ -1577,10 +1575,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,14 +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()
|
||||
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
when map_size(callbacks) == 0 do
|
||||
context
|
||||
@@ -406,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
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
|
||||
defmodule Module.ParallelChecker do
|
||||
@moduledoc false
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
import Kernel, except: [spawn: 3]
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type error() :: term()
|
||||
@type mode() :: :erlang | :elixir | :protocol
|
||||
|
||||
@typedoc """
|
||||
@@ -20,7 +18,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()}
|
||||
]
|
||||
|
||||
@@ -65,40 +62,20 @@ defmodule Module.ParallelChecker do
|
||||
@doc """
|
||||
Spawns a process that runs the parallel checker.
|
||||
"""
|
||||
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
|
||||
def spawn({pid, {checker, table}}, module, module_map, 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)
|
||||
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 +86,46 @@ 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: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
cache_from_module_map(table, module_map)
|
||||
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 +133,7 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end)
|
||||
|
||||
register_cache(checker, module, spawned, ref)
|
||||
register(checker, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -182,51 +177,54 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives pairs of module maps and BEAM binaries.
|
||||
|
||||
Returns the updated list of warnings from the verification.
|
||||
Receives pairs of module maps and BEAM binaries. In parallel it verifies
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
list of warnings from the verification.
|
||||
"""
|
||||
@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 +261,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 +275,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 +337,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 +364,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)
|
||||
@@ -430,7 +423,7 @@ defmodule Module.ParallelChecker do
|
||||
mode =
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
|
||||
{:elixir_checker_v3, contents} <- :erlang.binary_to_term(chunk) do
|
||||
# The chunk has more information, so that's our preference
|
||||
cache_chunk(table, module, contents)
|
||||
else
|
||||
@@ -481,12 +474,12 @@ defmodule Module.ParallelChecker do
|
||||
if Keyword.has_key?(attributes, :__protocol__), do: :protocol, else: :elixir
|
||||
end
|
||||
|
||||
defp cache_from_module_map(table, map, signatures) do
|
||||
defp cache_from_module_map(table, map) do
|
||||
exports =
|
||||
behaviour_exports(map) ++
|
||||
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
|
||||
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), signatures)
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures)
|
||||
{elixir_mode(map.attributes), module_map_to_module_tuple(map)}
|
||||
end
|
||||
|
||||
@@ -533,12 +526,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 +570,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 +582,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 +625,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 +637,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 +650,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
|
||||
|
||||
+97
-362
@@ -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.
|
||||
@@ -46,8 +49,8 @@ defmodule Module.Types do
|
||||
finder =
|
||||
fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, kind, _, _} = def ->
|
||||
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
|
||||
{_, kind, _, _} = clause ->
|
||||
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
|
||||
|
||||
false ->
|
||||
false
|
||||
@@ -72,27 +75,23 @@ 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)
|
||||
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
|
||||
end
|
||||
|
||||
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
|
||||
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
|
||||
{[{fun_arity, 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
|
||||
@@ -147,24 +146,12 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
|
||||
defp default_domain({_, arity} = fun_arity, impl) do
|
||||
with {for, callbacks} <- impl,
|
||||
true <- fun_arity in callbacks do
|
||||
args = [
|
||||
Descr.dynamic(Module.Types.Of.impl(for))
|
||||
| List.duplicate(Descr.dynamic(), arity - 1)
|
||||
]
|
||||
|
||||
{_fun_arity, kind, meta, clauses} = def
|
||||
|
||||
clauses =
|
||||
for {meta, args, guards, body} <- clauses do
|
||||
{[type_check: {:impl, for}] ++ meta, args, guards, body}
|
||||
end
|
||||
|
||||
{mode, {fun_arity, kind, meta, clauses}, args}
|
||||
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
|
||||
else
|
||||
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
|
||||
_ -> List.duplicate(Descr.dynamic(), arity)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -174,30 +161,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
|
||||
@@ -222,7 +208,7 @@ defmodule Module.Types do
|
||||
|
||||
finder = fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
|
||||
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
@@ -232,8 +218,8 @@ defmodule Module.Types do
|
||||
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
# Optimized version of finder, since we already have the definition
|
||||
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
|
||||
# Optimized version of finder, since we already the definition
|
||||
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
end)
|
||||
@@ -251,21 +237,15 @@ defmodule Module.Types do
|
||||
context ->
|
||||
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
{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 ->
|
||||
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
stack = %{stack | function: fun_arity}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
end
|
||||
@@ -295,11 +275,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}))
|
||||
@@ -312,245 +292,62 @@ 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.opt_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
|
||||
{_, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {Pattern.init_previous(), [], context}, fn
|
||||
{meta, args, guards, body}, {previous, 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)
|
||||
|
||||
head_args_types =
|
||||
case inferred do
|
||||
[] -> nil
|
||||
_ -> Pattern.of_domain(trees, stack, head_context)
|
||||
args_types =
|
||||
if stack.mode == :traversal do
|
||||
expected
|
||||
else
|
||||
Pattern.of_domain(trees, expected, context)
|
||||
end
|
||||
|
||||
args_triplet = {args_types, head_args_types, head_no_previous_args_types}
|
||||
inferred = [{args_triplet, return_type, precise?} | inferred]
|
||||
{previous, inferred, context}
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, args_types, return_type, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
|
||||
else
|
||||
{index + 1, total, [{index, type_index} | mapping], inferred, context}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
end)
|
||||
|
||||
{clauses_types, mapping, domain} =
|
||||
clauses_types
|
||||
|> Enum.reverse()
|
||||
|> group_clauses()
|
||||
|
||||
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, clauses_types}
|
||||
inferred = {:infer, domain, Enum.reverse(clauses_types)}
|
||||
{inferred, mapping, restore_context(clauses_context, context)}
|
||||
end
|
||||
|
||||
defp group_clauses(clauses) do
|
||||
{_, all_clauses, filtered_clauses, non_empty?} =
|
||||
Enum.reduce(clauses, {0, [], [], false}, fn
|
||||
{_args_triplet, return, precise?} = clause,
|
||||
{index, all_clauses, filtered_clauses, non_empty?} ->
|
||||
empty? = Descr.empty?(return)
|
||||
indexed_clause = {clause, index}
|
||||
|
||||
filtered_clauses =
|
||||
if precise? and empty? do
|
||||
filtered_clauses
|
||||
else
|
||||
[indexed_clause | filtered_clauses]
|
||||
end
|
||||
|
||||
{index + 1, [indexed_clause | all_clauses], filtered_clauses, non_empty? or not empty?}
|
||||
end)
|
||||
|
||||
clauses =
|
||||
if non_empty? do
|
||||
Enum.reverse(filtered_clauses)
|
||||
else
|
||||
Enum.reverse(all_clauses)
|
||||
end
|
||||
|
||||
[
|
||||
{{{args, _head_args, _head_no_previous_args}, _return, _precise?}, _index}
|
||||
| clauses_tail
|
||||
] = clauses
|
||||
|
||||
domain =
|
||||
Enum.reduce(clauses_tail, args, fn
|
||||
{{{args, head_args, head_no_previous_args}, _return, _precise?}, _index}, domain ->
|
||||
compute_domain(args, head_args, head_no_previous_args, domain)
|
||||
end)
|
||||
|
||||
{_, mapping, inferred} =
|
||||
Enum.reduce(clauses, {0, [], []}, fn
|
||||
{{{args, _head_args, _head_no_previous_args}, return, _precise?}, index},
|
||||
{total, mapping, inferred} ->
|
||||
{type_index, inferred} = add_inferred(inferred, args, return, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{total + 1, [{index, total} | mapping], inferred}
|
||||
else
|
||||
{total, [{index, type_index} | mapping], inferred}
|
||||
end
|
||||
end)
|
||||
|
||||
{Enum.reverse(inferred), mapping, domain}
|
||||
end
|
||||
|
||||
defp compute_domain(
|
||||
[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:
|
||||
#
|
||||
# opt_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.opt_union(Descr.upper_bound(no_prev_arg), d)
|
||||
else
|
||||
Descr.opt_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),
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])}
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
|
||||
|
||||
defp add_inferred([head | tail], args, return, index, acc),
|
||||
do: add_inferred(tail, args, return, index - 1, [head | acc])
|
||||
@@ -558,58 +355,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.opt_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}
|
||||
@@ -672,8 +417,9 @@ defmodule Module.Types do
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :except_none | :include_none)
|
||||
reverse_arrow: nil
|
||||
# Control if variable refinement is enabled.
|
||||
# It is disabled only on dynamic dispatches.
|
||||
refine_vars: false
|
||||
}
|
||||
end
|
||||
|
||||
@@ -684,45 +430,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
|
||||
|
||||
|
||||
+284
-1567
File diff suppressed because it is too large
Load Diff
+2469
-4754
File diff suppressed because it is too large
Load Diff
+407
-706
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.
|
||||
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
|
||||
## Warnings
|
||||
|
||||
@doc """
|
||||
Converts an integer into ordinal.
|
||||
Converts an itneger into ordinal.
|
||||
"""
|
||||
def integer_to_ordinal(i) do
|
||||
case rem(i, 10) do
|
||||
@@ -91,6 +91,29 @@ defmodule Module.Types.Helpers do
|
||||
"var.fun()" (with parentheses) means "var" is an atom()
|
||||
"""
|
||||
|
||||
:interpolation ->
|
||||
"""
|
||||
|
||||
#{hint()} string interpolation uses the String.Chars protocol to \
|
||||
convert a data structure into a string. Either convert the data type into a \
|
||||
string upfront or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:generator ->
|
||||
"""
|
||||
|
||||
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
|
||||
data structures. Either convert the data type into a list (or another Enumerable) \
|
||||
or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:into ->
|
||||
"""
|
||||
|
||||
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
|
||||
build its result. Either pass a valid data type or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:anonymous_rescue ->
|
||||
"""
|
||||
|
||||
@@ -109,20 +132,6 @@ defmodule Module.Types.Helpers do
|
||||
the union (which may be none)
|
||||
"""
|
||||
|
||||
{:impl, for} ->
|
||||
# Get the type without dynamic for better pretty printing
|
||||
type =
|
||||
for
|
||||
|> Module.Types.Of.impl()
|
||||
|> Module.Types.Descr.dynamic()
|
||||
|> Map.fetch!(:dynamic)
|
||||
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
|
||||
|
||||
"""
|
||||
|
||||
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
|
||||
"""
|
||||
|
||||
:empty_domain ->
|
||||
"""
|
||||
|
||||
@@ -143,13 +152,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 +169,9 @@ defmodule Module.Types.Helpers do
|
||||
_ ->
|
||||
{:ok, versions}
|
||||
end
|
||||
else
|
||||
_ -> {node, versions}
|
||||
end
|
||||
|
||||
node, versions ->
|
||||
{node, versions}
|
||||
end)
|
||||
|
||||
versions
|
||||
@@ -262,8 +270,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 +336,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 +361,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]]}
|
||||
|
||||
+224
-492
@@ -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.
|
||||
@@ -11,11 +10,11 @@ defmodule Module.Types.Of do
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float opt_union(integer(), float())
|
||||
@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 purposely 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 stack do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
|
||||
new_type = opt_intersection(old_type, type)
|
||||
|
||||
case empty?(new_type) do
|
||||
true when reverse_arrow == :include_none ->
|
||||
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,18 +81,13 @@ 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 = opt_intersection(type, old_type)
|
||||
new_type = intersection(type, old_type)
|
||||
|
||||
data = %{
|
||||
data
|
||||
@@ -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,76 +124,16 @@ 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
|
||||
opt_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()},
|
||||
{List, opt_union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})},
|
||||
{List, union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: if_set(negation(atom())))},
|
||||
{Port, port()},
|
||||
{PID, pid()},
|
||||
{Reference, reference()},
|
||||
@@ -273,28 +141,17 @@ defmodule Module.Types.Of do
|
||||
{Any, term()}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Currently, for protocol implementations, we only store
|
||||
the open struct definition. This is because we don't want
|
||||
to reconsolidate whenever the struct changes, but at the
|
||||
moment we can't store references either. Ideally struct
|
||||
types on protocol dispatches would be lazily resolved.
|
||||
"""
|
||||
def impl(for, mode \\ :closed)
|
||||
|
||||
for {for, type} <- impls do
|
||||
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
|
||||
def impl(unquote(for)), do: unquote(Macro.escape(type))
|
||||
end
|
||||
|
||||
def impl(struct, mode) do
|
||||
# Elixir did not strictly require the implementation to be available,
|
||||
# so we need to deal with such cases accordingly.
|
||||
def impl(struct) do
|
||||
# Elixir did not strictly require the implementation to be available, so we need a fallback.
|
||||
# TODO: Assume implementation is available on Elixir v2.0.
|
||||
# A warning is emitted since v1.19+.
|
||||
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
struct_type(struct, info)
|
||||
else
|
||||
open_map(__struct__: {atom([struct]), false})
|
||||
open_map(__struct__: atom([struct]))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -304,7 +161,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,144 +176,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, false}} | 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] ->
|
||||
products = cartesian_map(tail)
|
||||
|
||||
for key <- keys, product <- products do
|
||||
closed_map(non_multiple ++ [{key, {type, false}} | product])
|
||||
end
|
||||
|> Enum.reduce(&opt_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, optional?}} ->
|
||||
{key, {opt_union(old_type, new_type), optional?}}
|
||||
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, opt_union(old_type, new_type)} end)
|
||||
|
||||
{single, multiple}
|
||||
{map, context}
|
||||
end
|
||||
|
||||
defp union_negated(negated, new_type, single, multiple) do
|
||||
{single, matched} =
|
||||
Enum.map_reduce(single, [], fn
|
||||
{key, {old_type, optional?}}, matched ->
|
||||
if key in negated do
|
||||
{{key, {old_type, optional?}}, [key | matched]}
|
||||
else
|
||||
{{key, {opt_union(old_type, new_type), optional?}}, matched}
|
||||
end
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, opt_union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple}
|
||||
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,
|
||||
{_optional?, 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
|
||||
@@ -465,66 +285,56 @@ defmodule Module.Types.Of do
|
||||
[[]]
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
products = cartesian_map(tail)
|
||||
for key <- keys, product <- products, do: [{key, {type, false}} | product]
|
||||
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
|
||||
end
|
||||
end
|
||||
|
||||
@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
|
||||
{_optional?, 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, false}}, context}
|
||||
end)
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
|
||||
{closed_map([__struct__: {atom([struct]), false}] ++ 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
|
||||
|
||||
@@ -535,87 +345,51 @@ defmodule Module.Types.Of do
|
||||
# we introduce typed structs. They are only used by exceptions.
|
||||
def struct_type(struct, info, args_types \\ []) do
|
||||
term = dynamic()
|
||||
pairs = for %{field: field} <- info, do: {field, {term, false}}
|
||||
pairs = [{:__struct__, {atom([struct]), false}} | pairs]
|
||||
|
||||
pairs =
|
||||
if args_types == [] do
|
||||
pairs
|
||||
else
|
||||
pairs ++ args_types
|
||||
end
|
||||
|
||||
pairs = for %{field: field} <- info, do: {field, term}
|
||||
pairs = [{:__struct__, atom([struct])} | pairs]
|
||||
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
|
||||
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} =
|
||||
@@ -632,26 +406,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
|
||||
|
||||
@@ -667,48 +425,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
|
||||
@@ -729,12 +478,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)}
|
||||
@@ -747,6 +493,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: []
|
||||
@@ -866,37 +629,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user