Compare commits

..
79 Commits
Author SHA1 Message Date
jamauro 372836d623 Change variable from bk to bucket (#13921)
Simple change from `bk` to `bucket` for consistency with the rest of the doc
2024-10-21 18:00:21 +02:00
Jean Klingler 444cd7e6bd Fix crash in Macro.to_string/1 on invalid sigils (#13905) 2024-10-17 15:39:57 +09:00
Jean Klingler cacb175ab3 Add @doc false to Inspect.Algebra.no_limit/1 (#13893) 2024-10-10 18:21:49 +09:00
Jean Klingler bfce3ed7a9 Fix another dialyzer Kernel.with edge case (#13869)
Close https://github.com/elixir-lang/elixir/issues/13868
2024-09-30 20:11:05 +02:00
Vasily Suvorov 06811cbdc8 Fix typedoc for init_option (#13870)
Change `c:init/1` to`init/2` in the `typedoc`'s text
2024-09-30 15:41:35 +02:00
Igor Goryachev 7a6b35422c Fix logger translator tests under Erlang 27.1 (#13839) 2024-09-21 13:36:09 +02:00
José Valim 78f63d0831 Release v1.17.3 2024-09-18 14:33:39 +02:00
Urmas Talimaa 86ec46a5c4 Fix parsing of fractional durations (#13832)
The parsing of fractional durations checked for non-negativity by
testing second > 0, which reports false for not only negative integers
but also for 0.

Note that changing `if second > 0` to `if second >= 0` would fix
behaviour for "PT0,6S", but would break "PT-0,6S".
2024-09-18 09:03:51 +02:00
José Valim c99631876c Exclude latest tags 2024-09-17 18:01:17 +02:00
José Valim 46815ef554 Fix docs node generation 2024-09-17 16:40:01 +02:00
José Valim 256a55a53e Do not verify modules twice 2024-09-17 16:22:38 +02:00
Jean Klingler a78c92043f Fix: Macro.expand/2 should not expand __ENV__ on :match context (#13807) 2024-09-06 17:58:28 +09:00
José Valim b3bfa519a7 Pass proper path to consolidation in IEx recompile 2024-08-20 11:15:30 +02:00
Steve Cohen b734da3f62 Ensure :native is a valid parameter type (#13781)
Several of the functions in System that deal with time had a typespec
that only allowed the time_unit type even though they accepted the
:native time unit. This confuses dialyzer if :native is passed to
these functions
2024-08-17 10:53:36 +09:00
José Valim 10f86595b5 Raise clearer error message if struct is missing on type checker 2024-08-09 13:05:28 +02:00
José Valim 1da385776f More docs 2024-08-07 01:06:57 +02:00
José Valim a45e08eeda Keep labels with at least one occurrence on xref graph cycles and stats.
Closes #13762.
2024-08-07 00:57:21 +02:00
Artur Plysiuk bb61b953d7 Fix spec for to_timeout/1 (#13755) 2024-08-02 12:15:17 +02:00
José Valim 1e126e88b7 Do not attempt to group module warnings with context
Otherwise, the larger the context, the more expensive
grouping the warnings would get.

Closes #13742
2024-07-28 20:12:15 +02:00
José Valim 47abe2d107 Release v1.17.2 2024-07-06 23:10:25 +02:00
José Valim e254e7e68e Fix regression when fetching repo with a ref
Also note that depth and ref cannot be used in combination.
2024-07-02 20:09:54 +02:00
Wojtek Mach d3c0b38c5d Improvements to generated rel/bin/app.bat (#13679)
* Validate value of `!RELEASE_MODE!`

  * Exit with `ERRORLEVEL` set to `1` on errors.

    Previously we did not and so apps that use releases and rely on exit
    codes to report errors were not doing so (e.g. Livebook).

  * Rename rpc node name from `rem-(...)` to `rpc-(...)`

  * Update generated `rel/env.bat.exs`
2024-07-02 15:21:38 +02:00
José Valim 662dd6b8d9 Add CHANGELOG entry on jaro_distance, closes #13696 2024-06-27 09:14:53 +02:00
José Valim cadd501cbd Clarify cookie generation, closes #13694 2024-06-26 13:28:57 +02:00
Alexey Nikitin ddf84aba7e Fix vm.args.eex generated file when remote? is false (#13688)
before
```
-start_epmd false -erl_epmd_port 6789false
```

after
```
-start_epmd false -erl_epmd_port 6789
```
2024-06-24 09:47:21 +02:00
Jean Klingler 2d808ec652 Fix logger crash when :gen_statem format_status/2 returns non-tuple (#13684) 2024-06-22 20:14:56 +09:00
José Valim 6bfb95ab88 Release v1.17.1 2024-06-18 13:11:00 +02:00
José Valim d53977002d Print type violation slogan 2024-06-18 12:46:59 +02:00
José Valim 776e5a34ea Do not flip async/sync modules in --repeat-until-failure 2024-06-15 18:00:29 +02:00
Jean Klingler ae5707f90a Fix bug when using pinned variable in with else block (#13667) 2024-06-15 18:49:37 +09:00
José Valim 67617e5d8c Do not warn for assignment with blocks in EEx 2024-06-15 11:05:45 +02:00
José Valim 6f5fc94276 Clarify docs, closes #13666 2024-06-15 10:20:40 +02:00
José Valim 6c935ca6ec Clarify charlists intro, closes #13663 2024-06-14 17:34:27 +02:00
José Valim d1076e487c Do not run fixpoint computations on runtime deps
If A depends on B and B depends on C, all as runtime
dependencies, and C changes, there is no change that
can happen in C that will change the result of running
the checker on A.

This could change in the future in two ways:

1. If we introduce inference of return types, then we
   need to infer and check the types for A again. This
   may be an argument for not introducing inference of
   return types (only of patterns and guards)

2. We will need to track "type exports" once we introduce
   type aliases
2024-06-14 12:07:02 +02:00
Jonatan Kłosko b9e447757d Update RELEASE_DISTRIBUTION docs (#13658) 2024-06-13 16:15:45 +02:00
Jean Klingler b7943e113d Fix dialyzer error when with else clause is calling a no_return function (#13659)
Close #13656
2024-06-13 16:15:32 +02:00
Ben Murden 1a320bb60d Minor grammatical fixes on warning message (#13654) 2024-06-13 16:15:25 +02:00
Ian Harris 64e63b1b9d Update compatibility-and-deprecations.md (#13657)
Fixed link for changelog to v1.17.
2024-06-12 19:12:55 +02:00
José Valim 5a8b28427c Update CHANGELOG 2024-06-12 15:20:57 +02:00
José Valim a1eb59d035 Remove /main link from CHANGELOG 2024-06-12 13:54:10 +02:00
José Valim 0b8623a032 Release v1.17.0 2024-06-12 12:23:37 +02:00
José Valim 1f187b529e Clarify typespecs handling 2024-06-11 21:13:57 +02:00
José Valim bde5b2d2fc s/detail/details 2024-06-10 22:38:23 +02:00
Maksym Verbovyi 357ffc497d Correct example in the docs (#13638) 2024-06-10 16:38:08 +02:00
Lukasz Samson 48338bc093 Fix crash when capturing ambiguous import 2024-06-10 16:37:34 +02:00
José Valim d547721c41 Add detail and span to typing diagnostics
Closes #13646.
2024-06-10 16:14:27 +02:00
Aleksei Matiushkin 615bb55e81 format_elixir: true option for file generation (#13644) 2024-06-06 18:35:54 +02:00
Jean Klingler 278cf65bb1 Revert types from #13552 (#13645) 2024-06-06 23:03:39 +09:00
Jean Klingler 125d55527d Add more information to anonymous rescue hint (#13641) 2024-06-06 21:15:48 +09:00
Travis Vander Hoop 9ffe44758c Update case.ex with correct :since for register_test/6 (#13642)
while addressing the `ExUnit.Case.register_test/3` deprecation in https://github.com/elixir-wallaby/wallaby/pull/776, I wondered how long `register_test/6` had been around. In looking at the source and the docs, I uncovered what I believe is an incorrect `since` attribute
2024-06-06 14:05:44 +02:00
Theodor Fiedler e2032e8dc4 Extend Duration shift documentation (#13640) 2024-06-06 00:05:22 +02:00
José Valim ac64fba4eb Release v1.17.0-rc.1 2024-06-03 14:40:04 +02:00
José Valim 78b7239eb7 Update CHANGELOG 2024-06-03 14:01:25 +02:00
Jean Klingler 021a698d5d Add :report option to aggregate results over multiple processes (#13632) 2024-06-03 20:48:15 +09:00
José Valim b3c3e1399d Use __info__ callback directly (#13630) 2024-06-02 16:17:27 +02:00
Łukasz Samson 18200af534 Allow to override the info callback in Macro.Env.define_import (#13628) 2024-06-02 16:17:24 +02:00
José Valim b63312ac2b Optimize :term (#13625) 2024-06-02 15:00:08 +02:00
José Valim db89cbf7e6 Avoid discarding information on conversion to #elixir_ex 2024-06-01 17:57:54 +02:00
José Valim 53c93b91da Avoid nesting of capture inside macros
Unfortunately this makes it so the unused capture
warnings emit false positives, so this particular
warning was removed.

Closes #13609.
2024-06-01 17:09:47 +02:00
José Valim a7bf120756 Fix race in registry tests from duplicate names 2024-05-31 22:37:50 +02:00
José Valim b56a907297 Type check the function type on fun.() 2024-05-30 20:37:41 +02:00
José Valim c7dfc33705 Simplify iso conversion, prefer Duration.new! 2024-05-29 00:06:37 +02:00
José Valim 4b1b852880 Remove code comment from release example 2024-05-29 00:06:34 +02:00
Tangui f9939a7029 Implement Duration.to_iso8601/1 (#13604) 2024-05-29 00:06:27 +02:00
Jean Klingler 0d6ae23e39 Add sort :per_call option to tprof (#13611) 2024-05-29 07:02:20 +09:00
Theodor Fiedler cf84b7b042 Support plus sign prefix in Calendar.ISO.parse_duration/1 (#13613) 2024-05-28 21:13:12 +02:00
Theodor Fiedler b84dd54ca1 Support negative period prefix in Calendar.ISO.parse_duration/1 (#13608) 2024-05-27 23:05:57 +02:00
Jean Klingler bb779221ff Implement mix profile.tprof (#13605) 2024-05-27 21:33:06 +02:00
Łukasz Samson be6ed1709d Keep position meta on & capture special variables in expanded AST (#13607) 2024-05-27 21:32:55 +02:00
Theodor Fiedler 79c19dcb04 Add Duration.from_iso8601/1 (#13473) 2024-05-27 11:48:07 +02:00
José Valim 772c7b0884 Add Keyword.intersect/2-3 2024-05-26 10:59:18 +02:00
José Valim 6250a17d8c Update CHANGELOG.md 2024-05-26 09:50:37 +02:00
José Valim 73d7762563 Avoid double tracing on captures 2024-05-25 11:13:29 +02:00
Yuri Pereira Constante ebe347410c Generate cover HTML files in parallel (#13596) 2024-05-24 20:27:34 +02:00
Juergen Braungardt d74e6314e4 Fix documentation of option :stderr_to_stdout for System.cmd (#13597) 2024-05-24 19:58:12 +02:00
José Valim c9a2afc0ce Use Mix.shell().error() for compiler tasks warnings 2024-05-24 19:57:24 +02:00
José Valim e4a455eedc Update CHANGELOG.md 2024-05-24 14:33:37 +02:00
José Valim a2600eace1 Release v1.17.0-rc.0 2024-05-24 12:38:20 +02:00
José Valim 27b6ea82ae Branch out v1.17 2024-05-24 12:37:32 +02:00
396 changed files with 10467 additions and 30116 deletions
+2 -1
View File
@@ -17,5 +17,6 @@
# Float tests
float_assert: 1
]
],
normalize_bitstring_modifiers: false
]
+134
View File
@@ -0,0 +1,134 @@
name: builds.hex.pm
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
concurrency: builds_txt
jobs:
release_pre_built:
strategy:
fail-fast: true
max-parallel: 1
matrix:
include:
- otp: 25
otp_version: "25.3"
upload_generic_version: upload_generic_version
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Get tags
run: git fetch --tags origin
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: Utils.sh
run: |
cat << 'EOF' > utils.sh
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
EOF
chmod +x utils.sh
- name: Upload Docs to S3
if: ${{ matrix.build_docs }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
for f in doc/*; do
if [ -d "$f" ]; then
app=`echo $f | sed s/"doc\/"//`
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge "${surrogate_key}"
fi
done
- name: Upload Precompiled to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.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\"}"
purge builds/elixir/${{github.ref_name}}
fi
- name: Update builds txt
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
ref_name=${{ github.ref_name }}
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
touch builds.txt
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${{matrix.upload_generic_version}}" == "upload_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
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
purge builds/elixir/txt
+5 -5
View File
@@ -3,12 +3,12 @@ name: CI for Markdown content
on:
push:
branches:
- "main"
- 'main'
paths:
- "lib/**/*.md"
- 'lib/**/*.md'
pull_request:
paths:
- "lib/**/*.md"
- 'lib/**/*.md'
workflow_dispatch:
jobs:
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Check out the repository
@@ -27,7 +27,7 @@ jobs:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@v18.0.0
uses: DavidAnson/markdownlint-cli2-action@v16.0.0
with:
globs: |
lib/elixir/pages/**/*.md
+8 -13
View File
@@ -11,6 +11,7 @@ on:
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
@@ -18,16 +19,13 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
strategy:
fail-fast: false
matrix:
include:
- otp_version: "27.1"
deterministic: true
- otp_version: "27.1"
otp_latest: true
- otp_version: "27.0"
otp_latest: true
- otp_version: "26.0"
- otp_version: "25.3"
- otp_version: "25.0"
@@ -35,7 +33,7 @@ jobs:
development: true
- otp_version: maint
development: true
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
@@ -43,9 +41,6 @@ jobs:
- uses: erlef/setup-beam@v1
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
@@ -70,19 +65,19 @@ jobs:
cd ../elixir/
make docs
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
# Recompile System without .git
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
if: ${{ matrix.otp_latest }}
test_windows:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["25.3", "26.2", "27.3"]
runs-on: windows-2022
otp_version: ["25.3", "26.0"]
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
@@ -109,7 +104,7 @@ jobs:
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
+3 -3
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
notify:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@v4
@@ -18,8 +18,8 @@ jobs:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: "27.3"
elixir-version: "1.18.3"
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
+16 -304
View File
@@ -2,20 +2,16 @@ name: Release
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
contents: write
id-token: write
attestations: write
jobs:
create_draft_release:
@@ -24,7 +20,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
@@ -32,29 +27,8 @@ jobs:
--notes '' \
--draft \
${{ github.ref_name }}
- uses: actions/checkout@v4
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
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
fi
git tag $ref_name --force
git push origin $ref_name --force
build:
release_pre_built:
needs: create_draft_release
strategy:
fail-fast: true
matrix:
@@ -71,287 +45,25 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: "Attest docs provenance"
uses: actions/attest-build-provenance@v2
id: attest-docs-provenance
if: ${{ matrix.build_docs }}
with:
subject-path: "Docs.zip"
- name: "Copy docs provenance"
if: ${{ matrix.build_docs }}
run: cp "$ATTESTATION" Docs.zip.sigstore
env:
ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}"
- name: Create Docs Hashes
if: ${{ matrix.build_docs }}
run: |
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@v4
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@v4
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@v4
if: ${{ matrix.build_docs }}
with:
name: Docs
path: Docs.zip*
sign:
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [25, 26, 27]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@v4
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: "Attest release provenance"
uses: actions/attest-build-provenance@v2
id: attest-provenance
with:
subject-path: ${{ env.RELEASE_FILE }}
- name: "Copy release .zip provenance"
shell: bash
run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore"
env:
ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}"
- name: Create Release Hashes
if: matrix.flavor == 'windows'
shell: pwsh
run: |
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
- name: Create Release Hashes
if: matrix.flavor == 'linux'
shell: bash
run: |
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@v4
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
upload-release:
needs: [create_draft_release, build, sign]
runs-on: ubuntu-22.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Upload Pre-built
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
else
tag="${{ github.ref_name }}"
fi
gh release upload \
--repo ${{ github.repository }} \
--clobber \
"$tag" \
elixir-otp-*.zip \
elixir-otp-*.zip.sha{1,256}sum \
elixir-otp-*.zip.sigstore \
elixir-otp-*.exe \
elixir-otp-*.exe.sha{1,256}sum \
elixir-otp-*.exe.sigstore \
Docs.zip \
Docs.zip.sha{1,256}sum \
Docs.zip.sigstore
upload-builds-hex-pm:
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 }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Init purge keys file
run: |
touch purge_keys.txt
- name: Upload Precompiled to S3
run: |
ref_name=${{ github.ref_name }}
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
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')
unzip Docs.zip
for f in doc/*; do
if [ -d "$f" ]; then
app=$(echo "$f" | sed s/"doc\/"//)
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${AWS_S3_BUCKET}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "${surrogate_key}" >> ../purge_keys.txt
fi
done
- name: Update builds txt
run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
for sha256_file in $(find . -name 'elixir-otp-*.zip.sha256sum' | sed 's/^\.\///'); do
otp_version=$(echo "${sha256_file}" | sed -r 's/^elixir-otp-([[:digit:]]+)\.zip\.sha256sum/otp-\1/')
build_sha256=$(cut -d ' ' -f 1 "${sha256_file}")
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
done
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
--cache-control "public,max-age=3600" \
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
echo 'builds/elixir/txt' >> purge_keys.txt
- name: Flush cache
if: github.repository == 'elixir-lang/elixir'
run: |
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
for key in $(cat purge_keys.txt); do
purge "${key}"
done
gh release upload --clobber "${{ github.ref_name }}" \
elixir-otp-${{ matrix.otp }}.zip \
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
elixir-otp-${{ matrix.otp }}.exe \
elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
- name: Upload Docs to GitHub
if: ${{ matrix.build_docs }}
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload --clobber "${{ github.ref_name }}" \
Docs.zip \
Docs.zip.sha{1,256}sum
@@ -19,6 +19,8 @@ runs:
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS
shell: bash
@@ -32,6 +34,8 @@ runs:
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-${{ inputs.otp }}.exe .
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
@@ -42,7 +46,7 @@ runs:
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
fi
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
- uses: actions/checkout@v4
- uses: actions/checkout@v3
if: ${{ inputs.build_docs }}
with:
repository: elixir-lang/ex_doc
@@ -60,5 +64,6 @@ runs:
if: ${{ inputs.build_docs }}
shell: bash
run: |
git fetch --tags
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
+176 -448
View File
@@ -1,505 +1,233 @@
# Changelog for Elixir v1.18
# Changelog for Elixir v1.17
Elixir v1.18 is an impressive release with improvements across the two main efforts happening within the Elixir ecosystem right now: set-theoretic types and language servers. It also comes with built-in JSON support and adds new capabilities to its unit testing library. Here is a quick break down.
This release includes type inference of patterns to provide warnings for an initial set of constructs (binaries, maps, and atoms) within the same function. It also includes a new Duration data type to interact with Calendar types, support for Erlang/OTP 27, and many other improvements.
## Type system improvements
## Warnings from gradual set-theoretic types
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex":
This release introduces gradual set-theoretic types to infer types from patterns and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391).
At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language:
* `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()` - these types are indivisible. This means both `1` and `13` get the same `integer()` type.
* `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types.
* `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key.
* `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them.
We focused on atoms and maps on this initial release as they are respectively the simplest and the most complex types representations, so we can stress the performance of the type system and quality of error messages. Modelling these types will also provide the most immediate benefits to Elixir developers. Assuming there is a variable named `user`, holding a `%User{}` struct with an `address` field, Elixir v1.17 will emit the following warnings at compile-time:
* Pattern matching against a map or a struct that does not have the given key, such as `%{adress: ...} = user` (notice `address` vs `adress`)
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0`
* Attempting to invoke to call an anonymous function without an actual function, such as `user.()`
* Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]`
* Performing structural comparisons between non-overlapping types, such as `integer >= string`
* Building and pattern matching on binaries without the relevant specifiers, such as `<<name>>` (this warns because by default it expects an integer, it should have been `<<name::binary>>` instead)
* Attempting to rescue an undefined exception or a struct that is not an exception
* Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
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/).
## Erlang/OTP support
This release adds support for Erlang/OTP 27 and drops support for Erlang/OTP 24. We recommend Elixir developers to migrate to Erlang/OTP 26 or later, especially on Windows. Support for WERL (a graphical user interface for the Erlang terminal on Windows) will be removed in Elixir v1.18.
## Adding `Duration` and `shift/2` functions
Elixir introduces the `Duration` data type and APIs to shift dates, times, and date times by a given duration, considering different calendars and time zones.
```elixir
defmodule User do
defstruct [:age, :car_choice]
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
if car in car_choices do
{:ok, car}
else
{:error, :no_choice}
end
end
def drive(%User{}, _car_choices) do
{:error, :not_allowed}
end
end
iex> Date.shift(~D[2016-01-31], month: 2)
~D[2016-03-31]
```
Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`.
Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument:
Note the operation is called `shift` (instead of `add`) since working with durations does not obey properties such as associativity. For instance, adding one month and then one month does not give the same result as adding two months:
```elixir
User.drive({:ok, %User{}}, car_choices)
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
```
Here is the warning:
Still, durations are essential for building intervals, recurring events, and modelling scheduling complexities found in the world around us. For `DateTime`s, Elixir will correctly deal with time zone changes (such as Daylight Saving Time), but provisions are also available in case you want to surface conflicts (for example, you shifted to a wall clock that does not exist, because the clock has been moved forward by one hour). See `DateTime.shift/2` for examples.
```
warning: incompatible types given to User.drive/2:
User.drive({:ok, %User{age: nil, car_choice: nil}}, car_choices)
given types:
{:ok, %User{age: nil, car_choice: nil}}, empty_list()
but expected one of:
dynamic(%User{age: term(), car_choice: term()}), dynamic()
where "car_choices" was given the type:
# type: empty_list()
# from: lib/foo.ex:21:17
car_choices = []
typing violation found at:
│
22 │ User.drive({:ok, %User{}}, car_choices)
│ ~
│
└─ lib/foo.ex:22:10: Example.run/0
```
> The mismatched arguments are shown in red, if your terminal supports ANSI coloring.
And the next snippet will warn because the `:error` clause will never match, as that's not a valid return type of the `User.drive/2` call:
Finally, a new `Kernel.to_timeout/1` function has been added, which helps developers normalize durations and integers to a timeout used by Process APIs. For example, to send a message after one hour, one can now write:
```elixir
case User.drive(user, car_choices) do
{:ok, car} -> car
:error -> Logger.error("User cannot drive")
end
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
```
And here is the warning:
## v1.17.3 (2024-09-18)
```
warning: the following clause will never match:
:error
because it attempts to match on the result of:
User.drive(user, car_choices)
which has type:
dynamic({:ok, term()} or {:error, :no_choice} or {:error, :not_allowed})
typing violation found at:
│
26 │ :error -> Logger.error("User cannot drive")
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
│
└─ lib/foo.ex:26: Example.run/0
```
For more details on typing inference and the trade-offs made by the Elixir team, [see our official documentation](https://hexdocs.pm/elixir/1.18/gradual-set-theoretic-types.html#type-inference).
There are many other improvements to the type system, which we will go in detail within the official release. Meanwhile, here is a list summary of the overall improvements done to the type system:
* Type inference of patterns (typing inference of guards will be part of an upcoming release)
* Type checking of all language constructs, including local and remote calls, except `for`, `with`, and closures
* Type checking of all functions inlined by the compiler found in `Kernel`
* Type checking of all conversion functions inlined by the compiler
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
* Detection of clauses and patterns that will never match from `case`, `cond`, and `=`
* Detection of unused clauses in private functions
## ExUnit improvements
ExUnit now supports parameterized tests to run the same test module multiple times under different parameters.
For example, Elixir ships a local, decentralized and scalable key-value process storage called `Registry`. The registry can be partitioned and its implementation differs depending if partitioning is enabled or not. Therefore, during tests, we want to ensure both modes are exercised. With Elixir v1.18, we can achieve this by writing:
```elixir
defmodule Registry.Test do
use ExUnit.Case,
async: true,
parameterize: [
%{partitions: 1},
%{partitions: 8}
]
# ... the actual tests ...
end
```
ExUnit parameterizes whole test modules. If your modules are configured to run concurrently, as above, so will the parameterized ones.
ExUnit also comes with the ability of specifying test groups. While ExUnit supports running tests concurrently, those tests must not have shared state between them. However, in large applications, it may be common for some tests to depend on some shared state, and other tests to depend on a completely separate state. For example, part of your tests may depend on Cassandra, while others depend on Redis. Prior to Elixir v1.18, these tests could not run concurrently, but in v1.18 they might as long as they are assigned to different groups. Tests modules within the same group do not run concurrently, but across groups, they might.
With features like async tests, suite partitioning, and now grouping, Elixir developers have plenty of flexibility to make the most use of their machine resources, both in development and in CI.
## `mix format --migrate`
The `mix format` command now supports an explicit `--migrate` flag, which will convert constructs that have been deprecated in Elixir to their latest version. Because this flag rewrites the AST, it is not guaranteed the migrated format will always be valid when used in combination with macros that also perform AST rewriting.
As of this release, the following migrations are executed:
* Normalize parens in bitstring modifiers - it removes unnecessary parentheses in known bitstring modifiers, for example `<<foo::binary()>>` becomes `<<foo::binary>>`, or adds parentheses for custom modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
* Charlists as sigils - formats charlists as `~c` sigils, for example `'foo'` becomes `~c"foo"`.
* `unless` as negated `if`s - rewrites `unless` expressions using `if` with a negated condition, for example `unless foo do` becomes `if !foo do`.
More migrations may be added in future releases.
## JSON support
This release includes official support for JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards.
### Encoding
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows:
| **Elixir** | **JSON** |
|-----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types.
If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON:
```elixir
@derive {JSON.Encoder, only: [...]}
defstruct ...
```
### Decoding
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
| Number | `integer() \| float()` |
| Boolean | `true \| false` |
| Null | `nil` |
| String | `binary()` |
| Object | `%{binary() => _}` |
## Language server listeners
4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means.
At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project.
This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other.
These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE.
## Potential incompatibilities
This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25).
Furthermore, in order to support inference of patterns, Elixir will raise if it finds recursive variable definitions. This means patterns that never match, such as this one, will no longer compile:
def foo(x = {:ok, y}, x = y)
However, recursion of root variables (where variables directly point to each other), will also fail to compile:
def foo(x = y, y = z, z = x)
While the definition above could succeed (as long as all three arguments are equal), the cycle is not necessary and could be removed, as below:
def foo(x = y, y = z, z)
You may also prefer to write using guards:
def foo(x, y, z) when x == y and y == z
## v1.18.5 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
## v1.18.4 (2025-05-21)
This release includes initial support for Erlang/OTP 28, for those who want to try it out. In such cases, you may use Elixir v1.18.4 precompiled for Erlang/OTP 27, as it is binary compatible with Erlang/OTP 28. Note, however, that Erlang/OTP 28 no longer allows regexes to be defined in the module body and interpolated into an attribute. If you do this:
```elixir
@some_attribute ~r/foo/
def some_fun, do: @some_attribute
```
You must rewrite it to:
```elixir
def some_fun, do: ~r/foo/
```
### 1. Enhancements
#### IEx
* [IEx.Helpers] Add `IEx.Helpers.process_info/1` which prints process information
#### Mix
* [mix compile] Support the `--no-listeners` option
* [mix local] Retry HTTP requests with disabled middlebox comp mode depending on the failure reason
* [mix local.hex] Install Hex per OTP release
* [mix local.rebar] Install Hex per OTP release
* [mix run] Support the `--no-listeners` option
### 2. Bug fixes
### 1. Bug fixes
#### Elixir
* [Kernel] Emit trace events for `@on_definition` callbacks
* [Kernel] Emit trace events for `@on_load` callbacks
* [Kernel] Emit trace events for `super` calls
* [Kernel] Emit trace events for imported function calls
* [Kernel] Optimize map unions to avoid building long lists
* [Kernel] Do not crash when type checking nested bitstrings in patterns
* [Kernel] Do not crash when non-binary bitstring is given as struct default value
* [Kernel] Recompile regexes when escaped from module attributes for Erlang/OTP 28 compatibility
* [Kernel] Preserve backwards compatibility in `elixir_erl`
* [Duration] Fix parsing of fractional durations with non-positive seconds
* [Kernel] Do not attempt to group module warnings when they have a large context
#### IEx
* [IEx.Helpers] Properly reconsolidate protocols on `recompile`
#### Mix
* [mix deps.get] Ensure git checkout works when there are untracked files in the dependency
* [mix loadpaths] Do not run listeners when not checking the deps
* [mix compile.elixir] Do not verify modules twice
* [mix xref] Respect the `--label` option on stats and cycles
## v1.18.3 (2025-03-06)
## v1.17.2 (2024-07-06)
### 1. Bug fixes
#### Logger
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
#### Mix
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
## v1.17.1 (2024-06-18)
### 1. Enhancements
#### Mix
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
### 2. Bug fixes
#### EEx
* [EEx] Do not warn for assignment with blocks in EEx
#### Elixir
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
#### ExUnit
* [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
## v1.17.0 (2024-06-12)
### 1. Enhancements
#### Elixir
* [JSON] Encode any JSON key to string
* [Kernel] Allow `<<_::3*8>>` in typespecs
* [Access] Add `Access.find/1` that mirrors `Enum.find/2`
* [Code] Support cursor inside fn/rescue/catch/else/after inside `Code.Fragment.container_cursor_to_quoted/2`
* [Date] Add `Date.shift/2` to shift dates with duration and calendar-specific semantics
* [Date] Allow `Date` to accept years outside of `-9999..9999` range
* [DateTime] Add `DateTime.shift/2` to shift datetimes with duration and calendar-specific semantics
* [Duration] Add a new `Duration` data type
* [GenServer] Add `c:GenServer.format_status/1` callback
* [Kernel] Add `Kernel.get_in/1` with safe nil-handling for access and structs
* [Kernel] Add `Kernel.is_non_struct_map/1` guard
* [Kernel] Add `Kernel.to_timeout/1`
* [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code
* [Kernel] Support integers in uppercase sigils
* [Keyword] Add `Keyword.intersect/2-3` to mirror the `Map` API
* [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages
* [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics
* [Process] Add `Process.set_label/1`
* [String] Add `String.byte_slice/3` to slice a string to a maximum number of bytes while keeping it UTF-8 encoded
* [System] Support `use_stdio: false` in `System.cmd/3` and `System.shell/2`
* [Time] Add `Time.shift/2` to shift times with duration and calendar-specific semantics
#### ExUnit
* [ExUnit] Propagate the test process itself as a caller in `start_supervised`
* [ExUnit] Include max cases in ExUnit reports
#### IEx
* [IEx.Helpers] Warns if `recompile` was called and the current working directory changed
* [IEx.Helpers] Add `c/0` as an alias to `continue/0`
* [IEx.Pry] Add `IEx.Pry.annotate_quoted/3` to annotate a quoted expression with pry breakpoints
#### Logger
* [Logger] Format `:gen_statem` reports using Elixir data structures
* [Logger] Include process label in logger events
#### Mix
* [mix loadpaths] Support `--no-listeners` option
* [mix deps] Add `:depth` option to `Mix.SCM.Git`, thus supporting shallow clones of Git dependencies
* [mix deps] Warn if `:optional` is used in combination with `:in_umbrella`
* [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped
* [mix deps.tree] Add `--umbrella-only` to `mix deps.tree`
* [mix profile.tprof] Add a new profiler, available on Erlang/OTP 27+, which can measure count, time, and heap usage
* [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run
* [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix test] Generate cover HTML files in parallel
### 2. Bug fixes
#### Elixir
* [CLI] Fix `--no-color` not setting `:ansi_enabled` to false
* [Protocol] Return correct implementation for an invalid struct pointing to `nil`
* [Stream] Do not raise when `Stream.cycle/1` is explicitly halted
#### ExUnit
* [ExUnit.Diff] Fix regression when diffing nested improper lists
* [bin/elixir.bat] Improve handling of quotes and exclamation marks in flags
* [Code] Address a bug where AST nodes for `(a -> b)` were not wrapped as part of the literal encoder
* [Kernel] Resolve inconsistencies of how `..` and `...` are handled at the AST level
* [Kernel] Fix parsing precedence of ambiguous operators followed by containers
* [Kernel] Do not expand code in `quote bind_quoted: ...` twice
* [Kernel] Respect `:line` property when `:file` is given as option to `quote`
* [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta
* [Kernel] Avoid double tracing events when capturing a function
* [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
#### IEx
* [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__`
* [IEx.Helpers] Do not purge on `recompile` if IEx is not running
## v1.18.2 (2025-01-22)
### 1. Enhancements
#### Elixir
* [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors
* [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments
* [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine
* [IEx.Helpers] Update the history size whenever history is pruned
#### Mix
* [mix] Print compilation lock waiting message to stderr
* [mix] Add an environment variable to optionally disable compilation locking
### 2. Bug fixes
#### Elixir
* [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang
#### ExUnit
* [ExUnit] Fix crash when diffing bitstring specifiers
#### IEx
* [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values
#### Mix
* [mix] Track compilation locks per user to avoid permission errors
* [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin
## v1.18.1 (2024-12-24)
### 1. Enhancements
* [Kernel] Do not emit type violation warnings when comparing or matching against literals
* [Kernel] Do not validate clauses of private overridable functions
### 2. Bug fixes
#### Elixir
* [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions
* [Kernel] Do not crash when typing violation is detected on dynamic dispatch
* [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation
* [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation
* [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases
* [Kernel] Ensure diagnostic traces are kept backwards compatible
#### ExUnit
* [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading
* [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups
#### Mix
* [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors`
* [mix deps.loadpaths] Add build lock
* [mix escript.build] Ensure build succeeds when protocol consolidation is disabled
* [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems
## v1.18.0 (2024-12-19)
### 1. Enhancements
#### Elixir
* [CLI] Add experimental PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows. Those provide a safer entry point for running Elixir from other platforms
* [Calendar] Add `Duration.to_string/1`
* [Code] Support several migration options in `Code.format_string!/2`
* [Code] Add parenthesis around `--` and `---` in `Code.format_string!/2` to make precedence clearer
* [Code] Include more metadata in `Code.string_to_quoted/2` when `token_metadata: true` to help compute ranges from the AST
* [Code.Fragment] Have `:capture_arg` as its own entry in `Code.Fragment.surround_context/2`
* [Config] Add `Config.read_config/1`
* [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2`
* [Exception] Add `MissingApplicationsError` exception to denote missing applications
* [JSON] Add a new `JSON` module with encoding and decoding functionality
* [JSON] Implement `JSON.Encoder` for all Calendar types
* [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well
* [Kernel] Warn on bidirectional confusability in identifiers
* [Kernel] Verify the type of the binary generators
* [Kernel] Track the type of tuples in patterns and inside `elem/2`
* [Kernel] Perform validation of root AST nodes in `unquote` and `unquote_splicing` to catch bugs earlier
* [Kernel] Add source, behaviour, and record information to Docs chunk metadata
* [Kernel] Support deterministic builds in tandem with Erlang by setting `ERL_COMPILER_OPTIONS=deterministic`. Keep in mind deterministic builds strip source and other compile time information, which may be relevant for programs
* [Kernel] Allow aliases and imports to be enabled conditionally in module body
* [List] Add `List.ends_with?/2`
* [Macro] Improve `dbg` handling of `if/2`, `with/1` and of code blocks
* [Macro] Add `Macro.struct_info!/2` to return struct information mirroring `mod.__info__(:struct)`
* [Registry] Add `Registry.lock/3` for local locking
* [PartitionSupervisor] Add `PartitionSupervisor.resize!/2` to resize the number of partitions in a supervisor (up to the limit it was started with)
* [Process] Handle arbitrarily high integer values in `Process.sleep/1`
* [Protocol] Add `@undefined_impl_description` to customize error message when an implementation is undefined
* [Protocol] Add `__deriving__/1` as optional macro callback to `Protocol`, no longer requiring empty implementations
* [String] Inspect special whitespace and zero-width characters using their Unicode representation
* [String] Update Unicode to 16.0
#### ExUnit
* [ExUnit] Support parameterized tests on `ExUnit.Case`
* [ExUnit] Support test groups: tests in the same group never run concurrently
* [ExUnit.Case] Add `test_pid` as a tag
#### IEx
* [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes
* [IEx] Add `:dot_iex` support to `IEx.configure/1`
* [IEx] Add report for normal/shutdown exits in IEx
#### Mix
* [mix compile] Ensure only a single operating system process can compile at a given time
* [mix deps.get] Ensure only a single operating system process can fetch deps at a given time
* [mix format] Add `mix format --migrate` to migrate from deprecated functionality
* [mix format] Add new options and metadata to improve formatting applying by editors and other environments
* [mix test] Taint failure manifest if requiring or compiling tests fail
* [Mix.Project] Add a `:listeners` configuration to listen to compilation events from the current and other operating system processes
* [Mix.Task.Compiler] Add API for fetching all persisted compiler diagnostics
* [Mix.Task.Compiler] Add API for fetching all compiler tasks
### 2. Bug fixes
#### Elixir
* [Code] Fix delimiter metadata for single quoted atoms and remote calls in `Code.string_to_quoted/2`
* [Code.Formatter] Fix formatter adding extra escapes to quoted remote calls
* [Code.Fragment] Properly handle keyword keys as their own entry
* [Inspect.Algebra] Ensure `next_break_fits` respects `line_length`
* [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler
* [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation
* [Module] Include module attribute line and name when tracing its aliases
* [Stream] Do not halt streams twice in `Stream.transform/5`
* [URI] Fix a bug when a schemaless URI is given to `URI.merge/2`
#### ExUnit
* [ExUnit.Assertions] Raise if guards are used in `assert/1` with `=`
* [ExUnit.Assertions] Format inserted/deleted maps in list assertions
#### IEx
* [IEx.Helpers] `IEx.Helpers.recompile/0` will reload modules changed by other operating system processes
#### Mix
* [mix compile] Ensure warnings from external resources are emitted with `--all-warnings` when files do not change
* [mix deps.compile] Fix escaping issues when invoking `rebar3` in some cases
* [mix escript] Fix escript layout and support storing `priv` directories
* [mix release] Make `.app` files deterministic in releases
* [Mix.Shell] Fix `Mix.Shell` on Windows when outputting non UTF-8 characters
* [mix deps] Fix error message for diverged SCM definition in sibling
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Inspect.Algebra] `color/3` is deprecated in favor of `color_doc/3`
* [Inspect.Algebra] `fold_doc/2` is deprecated in favor of `fold/2`
* [Kernel] Deprecate `unless` in favor of `if`. Use `mix format --migrate` to automate the migration
* [Macro] `Macro.struct!/2` is deprecated in favor of `Macro.struct_info!/2`
* [Protocol] Defining `__deriving__/3` inside the `Any` implementation is deprecated, derive it inside the protocol definition itself
### 4. Hard deprecations
#### EEx
* [EEx] `<%#` is deprecated in favor of `<%!--` or `<% #`
* [EEx] `c:EEx.handle_text/2` is deprecated in favor of `c:EEx.handle_text/3`
#### Elixir
* [Code] Setting `:warnings_as_errors` is deprecated via `Code.put_compiler_option/2`. This must not affect developers, as the `:warnings_as_errors` option is managed by Mix tasks, and not directly used via the `Code` module
* [Enumerable] Deprecate returning a two-arity function in `Enumerable.slice/1`
* [List] `List.zip/1` is deprecated in favor of `Enum.zip/1`
* [Module] Deprecate `Module.eval_quoted/3` in favor of `Code.eval_quoted/3`
* [Range] Deprecate inferring negative ranges on `Range.new/2`
* [Tuple] `Tuple.append/2` is deprecated, use `Tuple.insert_at/3` instead
* [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+
#### Mix
* [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP`
* [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]`
* [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]`
* [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0`
* [mix profile.cprof] Deprecated in favor of the new `mix profile.tprof`
* [mix profile.eprof] Deprecated in favor of the new `mix profile.tprof`
### 5. Potential breaking changes
### 4. Hard deprecations
#### Elixir
* [Kernel] Using unquote is deprecated when quote is used inside a pattern or guard
* [IO] Passing `:all` to `IO.read/2` and `IO.binread/2` is deprecated, pass `:eof` instead
* [Kernel] Single-quote charlists are deprecated, use `~c` instead
* [Kernel] Deprecate escaping closing delimiter in uppercase sigils
* [Range] `left..right` without explicit steps inside patterns and guards is deprecated, write `left..right//step` instead
* [Range] Decreasing ranges, such as `10..1` without an explicit step is deprecated, write `10..1//-1` instead
## v1.17
#### ExUnit
The CHANGELOG for v1.17 releases can be found [in the v1.17 branch](https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md).
* [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons
## v1.16
The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md).
+31 -28
View File
@@ -5,7 +5,7 @@ MAN_PREFIX ?= $(SHARE_PREFIX)/man
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
@@ -17,11 +17,11 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.PHONY: install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
.NOTPARALLEL:
#==> Functions
@@ -65,21 +65,17 @@ endef
#==> Compilation tasks
APP := lib/elixir/ebin/elixir.app
EEX := lib/eex/ebin/Elixir.EEx.beam
ELIXIR := lib/elixir/ebin/elixir.beam
PARSER := lib/elixir/src/elixir_parser.erl
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
default: compile
compile: erlang elixir
compile: erlang $(APP) elixir
erlang: $(ELIXIR)
$(ELIXIR): $(PARSER) lib/elixir/src/*
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
$(Q) $(GENERATE_APP) $(VERSION)
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -87,27 +83,29 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
# Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file,
# then Mix, and then compile EEx fully
elixir: stdlib $(EEX) mix ex_unit logger eex iex
stdlib: $(KERNEL) $(UNICODE) $(APP)
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
"$(MAKE)" unicode; \
fi
$(Q) "$(MAKE)" unicode
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
$(Q) "$(MAKE)" app
app: $(APP)
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(Q) $(GENERATE_APP) $(VERSION)
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
unicode: $(UNICODE)
$(UNICODE): lib/elixir/unicode/*
@ echo "==> unicode (compile)";
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -146,7 +144,6 @@ check_reproducible: compile
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
$(Q) rm -rf lib/*/ebin
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
$(Q) echo "Diffing..."
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
@@ -157,10 +154,16 @@ check_reproducible: compile
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible"
clean: clean_man
clean:
rm -rf ebin
rm -rf lib/*/ebin
rm -rf $(PARSER)
$(Q) "$(MAKE)" clean_residual_files
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/
rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/
@@ -170,9 +173,7 @@ clean: clean_man
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
$(Q) "$(MAKE)" clean_man
#==> Documentation tasks
@@ -304,15 +305,17 @@ dialyze: compile $(PLT)
build_man: man/iex.1 man/elixir.1
define BUILD_MANPAGES
man/$(APP).1:
$(Q) cp man/$(APP).1.in man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1
$(Q) rm -f man/$(APP).1.bak
endef
man/iex.1:
$(Q) cp man/iex.1.in man/iex.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) rm -f man/iex.1.bak
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
clean_man:
rm -f man/elixir.1
+1 -2
View File
@@ -3,10 +3,9 @@ LEGAL NOTICE INFORMATION
All the files in this distribution are copyright to the terms below.
== lib/elixir/src/elixir_json.erl
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
Copyright Ericsson AB 1996-2024
Copyright Ericsson AB 1996-2015
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+2 -11
View File
@@ -103,9 +103,7 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
In case you want to use this Elixir version as your system version,
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
When updating the repository, you may want to run `make clean` before
recompiling. For deterministic builds, you should set the environment
variable `ERL_COMPILER_OPTIONS=deterministic`.
Additionally, you may choose to run the test suite with `make clean test`.
## Contributing
@@ -139,20 +137,13 @@ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
Some test files need their `test_helper.exs` to be explicitly required
before, such as:
```sh
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile all (including Erlang modules):
To recompile (including Erlang modules):
```sh
make compile
+2 -2
View File
@@ -2,7 +2,7 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
@@ -30,7 +30,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
2. Start new /CHANGELOG.md
+2 -2
View File
@@ -6,11 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.18 | Bug fixes and security patches
1.17 | Security patches only
1.17 | Bug fixes and security patches
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.18.5
1.17.3
+7 -3
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.18.5
ELIXIR_VERSION=1.17.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -18,13 +18,13 @@ Usage: $(basename "$0") [options] [.exs file] [data]
-pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
--color, --no-color Enables or disables ANSI coloring
--erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--no-halt Does not halt the Erlang VM after execution
--short-version Prints Elixir version (standalone)
--werl Uses Erlang's Windows shell GUI (Windows only)
Options given after the .exs file or -- are passed down to the executed code.
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
@@ -112,7 +112,7 @@ while [ $I -le $LENGTH ]; do
C=1
MODE="iex"
;;
-v|--no-halt|--color|--no-color)
-v|--no-halt)
C=1
;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
@@ -182,6 +182,10 @@ while [ $I -le $LENGTH ]; do
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
fi
;;
--werl)
S=1
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
;;
*)
while [ $I -le $LENGTH ]; do
I=$((I + 1))
+17 -6
View File
@@ -1,6 +1,6 @@
@echo off
set ELIXIR_VERSION=1.18.5
set ELIXIR_VERSION=1.17.3
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
@@ -24,13 +24,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo.
echo --color, --no-color Enables or disables ANSI coloring
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --no-halt Does not halt the Erlang VM after execution
echo --short-version Prints Elixir version (standalone)
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Options given after the .exs file or -- are passed down to the executed code.
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
@@ -95,6 +95,7 @@ if "!par!"=="" (
shift
set par="!par:"=\"!"
rem ******* EXECUTION OPTIONS **********************
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
rem ******* ELIXIR PARAMETERS **********************
@@ -108,8 +109,6 @@ if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--color=! (goto startloop)
if ""==!par:--no-color=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
@@ -129,6 +128,10 @@ if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on
:run
setlocal disabledelayedexpansion
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true %beforeExtra%
)
if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
)
@@ -136,9 +139,17 @@ if not defined useIEx (
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
if defined ELIXIR_CLI_DRY_RUN (
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
echo start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
) else (
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
)
exit /B %ERRORLEVEL%
:end
+4 -1
View File
@@ -16,11 +16,14 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help".
goto end
:run
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
Executable → Regular
+1 -1
View File
@@ -20,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
}
# Corrected arguments are ready to pass to batch file
& $mixBatPath $newArgs
& $mixBatPath $newArgs
+3 -16
View File
@@ -48,14 +48,8 @@ defmodule EEx.Compiler do
end
end
# TODO: Remove me on Elixir v2.0
# TODO: Deprecate this on Elixir v1.18
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
line: line,
column: column,
file: state.file
)
case expr(t, line, column + 3, state, []) do
{:error, message} ->
{:error, message, %{line: line, column: column}}
@@ -305,14 +299,6 @@ defmodule EEx.Compiler do
}
init = state.engine.init(opts)
if function_exported?(state.engine, :handle_text, 2) and
not function_exported?(state.engine, :handle_text, 3) do
IO.warn(
"#{inspect(state.engine)}.handle_text/2 is deprecated, implement handle_text/3 instead"
)
end
generate_buffer(tokens, init, [], state)
end
@@ -330,7 +316,8 @@ defmodule EEx.Compiler do
meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
# TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end
+54 -12
View File
@@ -126,21 +126,51 @@ defmodule EEx.TokenizerTest do
end
test "EEx comments" do
ExUnit.CaptureIO.capture_io(:stderr, fn ->
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end)
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end
test "EEx comments with do-end" do
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 19, line: 1}},
{:eof, %{column: 32, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
end
test "EEx comments inside do-end" do
exprs = [
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 31, line: 1}},
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
{:eof, %{column: 43, line: 1}}
]
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
exprs = [
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
{:text, ~c"bar", %{column: 46, line: 1}},
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
{:eof, %{column: 58, line: 1}}
]
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
{:ok, exprs}
end
test "EEx multi-line comments" do
@@ -291,6 +321,15 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with comment" do
exprs = [
{:text, ~c"\n123", %{column: 19, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with multi-line comment" do
exprs = [
{:comment, ~c" comment ", %{column: 3, line: 1}},
@@ -345,6 +384,9 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
{:error, message, %{column: 5, line: 1}}
assert EEx.tokenize(~c"<%# true ", @opts) ==
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
message = """
expected closing '--%>' for EEx expression
|
+9
View File
@@ -198,6 +198,15 @@ defmodule EExTest do
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
end
test "embedded code with comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
assert_eval(
"foo\n\nbar\n",
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
)
end
test "embedded code with multi-line comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
+2 -2
View File
@@ -9,8 +9,8 @@
warn_deprecated_function,
warn_obsolete_guard,
warn_exported_vars,
%% Enable this when we require Erlang/OTP 27+
%% warnings_as_errors,
%% warn_missing_spec,
%% warn_untyped_record,
debug_info,
{outdir, "ebin/"}
]}.
+3 -3
View File
@@ -350,7 +350,7 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must access a list element by index, \
you can use Enum.at/2 or the functions in the List module\
you can Enum.at/1 or the functions in the List module\
"""
end
@@ -418,8 +418,8 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must modify a list element by index, \
you can use Access.at/1 or the functions in the List module\
as a whole. If you really must mostify a list element by index, \
you can Access.at/1 or the functions in the List module\
"""
end
+5 -5
View File
@@ -207,7 +207,7 @@ defmodule Agent do
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -284,7 +284,7 @@ defmodule Agent do
instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state.
"""
@spec start_link(module, atom, [term], GenServer.options()) :: on_start
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
def start_link(module, fun, args, options \\ []) do
GenServer.start_link(Agent.Server, {module, fun, args}, options)
end
@@ -311,7 +311,7 @@ defmodule Agent do
See `start_link/4` for more information.
"""
@spec start(module, atom, [term], GenServer.options()) :: on_start
@spec start(module, atom, [any], GenServer.options()) :: on_start
def start(module, fun, args, options \\ []) do
GenServer.start(Agent.Server, {module, fun, args}, options)
end
@@ -348,7 +348,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get(agent, module, atom, [term], timeout) :: term
@spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
end
@@ -389,7 +389,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get_and_update(agent, module, atom, [term], timeout) :: term
@spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
end
+12 -11
View File
@@ -54,12 +54,12 @@ defmodule Application do
You can also change the application environment dynamically by using functions
such as `put_env/3` and `delete_env/2`.
> #### Application environment in libraries {: .info}
> #### Environment in libraries {: .tip}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries).
> The config files `config/config.exs` and `config/runtime.exs`
> are rarely used by libraries. Libraries typically define their environment
> in the `application/0` function of their `mix.exs`. Configuration files
> are rather used by applications to configure their libraries.
> #### Reading the environment of other applications {: .warning}
>
@@ -673,6 +673,13 @@ defmodule Application do
> You must use this function to read only your own application
> environment. Do not read the environment of other applications.
> #### Application environment in libraries {: .info}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read our [library guidelines](library-guidelines.md).
## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications.
@@ -789,12 +796,6 @@ defmodule Application do
@doc """
Puts the `value` in `key` for the given `app`.
> #### Compile environment {: .warning}
>
> Do not use this function to change environment variables read
> via `Application.compile_env/2`. The compile environment must
> be exclusively set before compilation, in your config files.
## Options
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
+1 -2
View File
@@ -9,8 +9,7 @@ defmodule Behaviour do
attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
is documented in `Module`.
`MyModule.behaviour_info(:callbacks)` can be used.
"""
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
+10 -3
View File
@@ -1073,7 +1073,13 @@ defmodule Date do
def year_of_era(date)
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.year_of_era(year, month, day)
# TODO: Remove me on 1.17
# The behaviour implementation already warns on missing callback.
if function_exported?(calendar, :year_of_era, 3) do
calendar.year_of_era(year, month, day)
else
calendar.year_of_era(year)
end
end
@doc """
@@ -1153,11 +1159,12 @@ defmodule Date do
defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar != Calendar.ISO or year in -9999..9999 do
when year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar == Calendar.ISO do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
end
+5 -19
View File
@@ -28,12 +28,7 @@ defmodule DateTime do
## Time zone database
Many functions in this module require a time zone database.
A time zone database is a record of the UTC offsets that its locales have
used at various times in the past, are using, and are expected to use in the
future.
Because those plans can change, it needs to be periodically updated.
By default, `DateTime` uses the default time zone database returned by
By default, it uses the default time zone database returned by
`Calendar.get_time_zone_database/0`, which defaults to
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
datetimes and returns `{:error, :utc_only_time_zone_database}`
@@ -48,7 +43,7 @@ defmodule DateTime do
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
recommended for embedded devices
To use one of them, first make sure it is added as a dependency in `mix.exs`.
To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
@@ -1030,12 +1025,9 @@ defmodule DateTime do
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
Only supports converting datetimes which are in the ISO calendar,
attempting to convert datetimes from other calendars will raise.
You can also optionally specify an offset for the formatted string.
If none is given, the one in the given `datetime` is used.
Only supports converting datetimes which are in the ISO calendar.
If another calendar is given, it is automatically converted to ISO.
It raises if not possible.
WARNING: the ISO 8601 datetime format does not contain the time zone nor
its abbreviation, which means information is lost when converting to such
@@ -1349,11 +1341,6 @@ defmodule DateTime do
@doc """
Converts the given `datetime` to a string according to its calendar.
Unfortunately, there is no standard that specifies rendering of a
datetime with its complete time zone information, so Elixir uses a
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
@@ -2092,8 +2079,7 @@ defmodule DateTime do
)
case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
when calendar != Calendar.ISO or year in -9999..9999 ->
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
"~U[" <> formatted <> suffix(calendar) <> "]"
_ ->
+12 -118
View File
@@ -355,118 +355,11 @@ defmodule Duration do
end
end
@doc """
Converts the given `duration` to a human readable representation.
## Options
* `:units` - the units to be used alongside each duration component.
The default units follow the ISO 80000-3 standard:
[
year: "a",
month: "mo",
week: "wk",
day: "d",
hour: "h",
minute: "min",
second: "s"
]
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
## Examples
iex> Duration.to_string(Duration.new!(second: 30))
"30s"
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"40d 12h 42min 12s"
By default, this function uses ISO 80000-3 units, which uses "a" for years.
But you can customize all units via the units option:
iex> Duration.to_string(Duration.new!(year: 3))
"3a"
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
"3y"
You may also choose the separator:
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
"40d, 12h, 42min, 12s"
A duration without components is rendered as "0s":
iex> Duration.to_string(Duration.new!([]))
"0s"
Microseconds are rendered as part of seconds with the appropriate precision:
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
"1.002s"
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"-0.2000s"
"""
@doc since: "1.18.0"
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
case to_string_year(duration, [], units) do
[] ->
"0" <> Keyword.get(units, :second, "s")
[part] ->
IO.iodata_to_binary(part)
parts ->
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
end
end
defp to_string_part(0, _units, _key, _default, acc),
do: acc
defp to_string_part(x, units, key, default, acc),
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
defp to_string_year(%{year: year} = duration, acc, units) do
to_string_month(duration, to_string_part(year, units, :year, "a", acc), units)
end
defp to_string_month(%{month: month} = duration, acc, units) do
to_string_week(duration, to_string_part(month, units, :month, "mo", acc), units)
end
defp to_string_week(%{week: week} = duration, acc, units) do
to_string_day(duration, to_string_part(week, units, :week, "wk", acc), units)
end
defp to_string_day(%{day: day} = duration, acc, units) do
to_string_hour(duration, to_string_part(day, units, :day, "d", acc), units)
end
defp to_string_hour(%{hour: hour} = duration, acc, units) do
to_string_minute(duration, to_string_part(hour, units, :hour, "h", acc), units)
end
defp to_string_minute(%{minute: minute} = duration, acc, units) do
to_string_second(duration, to_string_part(minute, units, :minute, "min", acc), units)
end
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
acc
end
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc, units) do
[[second_component(s, ms, p) | Keyword.get(units, :second, "s")] | acc]
end
@doc """
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: `P3M3W3D`.
Note this function implements the *extension* of ISO 8601:2019. This extensions allows weeks to
appear between months and days: `P3M3W3D`, making it fully compatible with any `Duration` struct.
## Examples
@@ -514,15 +407,15 @@ defmodule Duration do
[]
end
defp second_component(%{second: 0, microsecond: {_, 0}}) do
~c"0S"
end
defp second_component(%{second: second, microsecond: {_, 0}}) do
[Integer.to_string(second), ?S]
end
defp second_component(%{second: second, microsecond: {ms, p}}) do
[second_component(second, ms, p), ?S]
end
defp second_component(second, _ms, 0) do
Integer.to_string(second)
end
defp second_component(second, ms, p) do
total_ms = second * @microseconds_per_second + ms
second = total_ms |> div(@microseconds_per_second) |> abs()
ms = total_ms |> rem(@microseconds_per_second) |> abs()
@@ -532,7 +425,8 @@ defmodule Duration do
sign,
Integer.to_string(second),
?.,
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p)
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p),
?S
]
end
+1 -1
View File
@@ -1710,7 +1710,7 @@ defmodule Calendar.ISO do
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _precision}
microsecond: {0, 0}
}) do
[
month: year * 12 + month,
+27 -46
View File
@@ -110,12 +110,30 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time(), :native)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
end
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO)
end
def utc_now(calendar) do
utc_now(:native, calendar)
calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
end
@doc """
@@ -140,20 +158,7 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
|> convert!(calendar)
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
end
@doc """
@@ -247,7 +252,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -312,7 +317,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(
@@ -754,9 +759,6 @@ defmodule NaiveDateTime do
@doc """
Converts the given naive datetime to a string according to its calendar.
For redability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
### Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
@@ -991,8 +993,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
{:ok, ~N[2000-01-01 13:30:15]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
{:ok, ~N[2000-01-01 13:30:15.005000]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
{:ok, ~N[2000-01-01 13:30:15.005]}
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
@@ -1001,11 +1001,7 @@ defmodule NaiveDateTime do
{:error, :invalid_date}
"""
@spec from_erl(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -1024,19 +1020,13 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
~N[2000-01-01 13:30:15]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
~N[2000-01-01 13:30:15.005000]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
"""
@spec from_erl!(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) :: t
@spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
@@ -1260,10 +1250,6 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert(
%{
calendar: calendar,
@@ -1465,15 +1451,10 @@ defmodule NaiveDateTime do
calendar: calendar
} = naive_datetime
if calendar != Calendar.ISO or year in -9999..9999 do
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> suffix(calendar) <> "]"
else
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
end
"~N[" <> formatted <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+12 -15
View File
@@ -115,7 +115,7 @@ defmodule Time do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -355,21 +355,13 @@ defmodule Time do
## Examples
iex> Time.from_erl({23, 30, 15})
{:ok, ~T[23:30:15]}
iex> Time.from_erl({23, 30, 15}, 5000)
{:ok, ~T[23:30:15.005000]}
iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time}
"""
@spec from_erl(
:calendar.time(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -385,8 +377,6 @@ defmodule Time do
iex> Time.from_erl!({23, 30, 15})
~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, 5000)
~T[23:30:15.005000]
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15})
@@ -531,7 +521,7 @@ defmodule Time do
do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a
if not valid? do
unless valid? do
raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
@@ -807,8 +797,15 @@ defmodule Time do
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
{:ok, value} = convert(time, calendar)
value
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
end
@doc """
+38 -99
View File
@@ -123,11 +123,6 @@ defmodule Code do
of the imported function/macro. A :remote_function/:remote_macro event
may still be emitted for the imported module/name/arity.
* `{:imported_quoted, meta, module, name, [arity]}` - traced whenever an
imported function or macro is processed inside a `quote/2`. `meta` is the
call AST metadata, `module` is the module the import is from, followed by
the `name` and a list of `arities` of the imported function/macro.
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
@@ -185,7 +180,7 @@ defmodule Code do
defmodule MyTracer do
def trace({:remote_function, _meta, module, name, arity}, env) do
IO.puts("#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}")
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
:ok
end
@@ -249,8 +244,8 @@ defmodule Code do
:debug_info,
:ignore_already_consolidated,
:ignore_module_conflict,
:infer_signatures,
:relative_paths
:relative_paths,
:warnings_as_errors
]
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@@ -512,9 +507,14 @@ defmodule Code do
## Options
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.
Options can be:
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
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
@@ -659,8 +659,6 @@ defmodule Code do
## Options
Regular options (do not change the AST):
* `:file` - the file which contains the string, used for error
reporting
@@ -684,34 +682,28 @@ defmodule Code do
If you set it to `false` later on, `do`-`end` blocks won't be
converted back to keywords.
Migration options (change the AST), see the "Migration formatting" section below:
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
* `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
becomes `<<foo::binary>>`, or adds parentheses for custom
modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
* `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
`'foo'` becomes `~c"foo"`.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_unless` (since v1.18.0) - when `true`,
rewrites `unless` expressions using `if` with a negated condition, for example
`unless foo, do:` becomes `if !foo, do:`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
## Design principles
The formatter was designed under three principles.
First, the formatter never changes the semantics of the code by default.
First, the formatter never changes the semantics of the code.
This means the input AST and the output AST are almost always equivalent.
The only cases where the formatter will change the AST is when the input AST
would cause *compiler warnings* and the output AST won't. The cases where
the formatter changes the AST can be disabled through formatting options
if desired.
The second principle is to provide as little configuration as possible.
This eases the formatter adoption by removing contention points while
@@ -945,8 +937,9 @@ defmodule Code do
## Code comments
The formatter handles code comments and guarantees a space is always added
between the beginning of the comment (#) and the next character.
The formatter also handles code comments in a way to guarantee a space
is always added between the beginning of the comment (#) and the next
character.
The formatter also extracts all trailing comments to their previous line.
For example, the code below
@@ -958,19 +951,9 @@ defmodule Code do
# world
hello
While the formatter attempts to preserve comments in most situations,
that's not always possible, because code comments are handled apart from
the code representation (AST). While the formatter can preserve code
comments between expressions and function arguments, the formatter
cannot currently preserve them around operators. For example, the following
code will move the code comments to before the operator usage:
foo() ||
# also check for bar
bar()
In some situations, code comments can be seen as ambiguous by the formatter.
For example, the comment in the anonymous function below
Because code comments are handled apart from the code representation (AST),
there are some situations where code comments are seen as ambiguous by the
code formatter. For example, the comment in the anonymous function below
fn
arg1 ->
@@ -999,21 +982,6 @@ defmodule Code do
## Newlines
The formatter converts all newlines in code from `\r\n` to `\n`.
## Migration formatting
As part of the Elixir release cycle, deprecations are being introduced,
emitting warnings which might require existing code to be changed.
In order to reduce the burden on developers when upgrading Elixir to the
next version, the formatter exposes some options, disabled by default,
in order to automate this process.
These options should address most of the typical use cases, but given they
introduce changes to the AST, there is a non-zero risk for meta-programming
heavy projects that relied on a specific AST, or projects that are
re-defining functions from the `Kernel`. In such cases, migrations cannot
be applied blindly and some extra changes might be needed in order to
address the deprecation warnings.
"""
@doc since: "1.6.0"
@spec format_string!(binary, keyword) :: iodata
@@ -1107,8 +1075,6 @@ defmodule Code do
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
* `:module` - the module to run the environment on
"""
@doc since: "1.14.0"
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@@ -1562,8 +1528,8 @@ defmodule Code do
## Examples
Code.compiler_options(infer_signatures: false)
#=> %{infer_signatures: true}
Code.compiler_options(warnings_as_errors: true)
#=> %{warnings_as_errors: false}
"""
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1592,12 +1558,6 @@ defmodule Code do
:elixir_config.get(key)
end
# TODO: Remove me in Elixir v2.0
def get_compiler_option(:warnings_as_errors) do
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
:ok
end
@doc """
Returns a list with all available compiler options.
@@ -1626,19 +1586,16 @@ defmodule Code do
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`.
This enables tooling to partially reconstruct the original source code,
for instance, to perform static analysis of code. Therefore, disabling
module. Defaults to `true`.
This enables static analysis tools as it allows developers to
partially reconstruct the original source code. Therefore, disabling
`:debug_info` is not recommended as it removes the ability of the
Elixir compiler and other tools to provide feedback. If you want to
remove the `:debug_info` while deploying, tools like `mix release`
already do such by default.
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.
Additionally, `mix test` disables it via the `:test_elixirc_options`
project configuration option.
This option can also be overridden per module using the `@compile` directive.
* `: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.
@@ -1647,19 +1604,13 @@ 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) - 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.
Defaults to `true`.
`mix test` automatically disables this option via the `:test_elixirc_options`
project configuration, as there is typically no need to store infer signatures
for test files.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* `:warnings_as_errors` - causes compilation to fail when warnings are
generated. Defaults to `false`.
* `: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
@@ -1705,18 +1656,6 @@ defmodule Code do
:ok
end
# TODO: Remove me in Elixir v2.0
def put_compiler_option(:warnings_as_errors, _value) do
IO.warn(
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
"instead you must pass it as a --warnings-as-errors flag. " <>
"If you need to set it as a default in a mix task, you can also set it under aliases: " <>
"[compile: \"compile --warnings-as-errors\"]"
)
:ok
end
def put_compiler_option(:no_warn_undefined, value) do
if value != :all and not is_list(value) do
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
@@ -1746,7 +1685,7 @@ defmodule Code do
:ok
end
# TODO: Remove this option on Elixir v2.0
# TODO: Make this option have no effect on Elixir v2.0
# TODO: Warn if mode is :warn on Elixir v1.19
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
:elixir_config.put(:on_undefined_variable, value)
+66 -160
View File
@@ -16,7 +16,7 @@ defmodule Code.Formatter do
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
# Operators that are composed of multiple binary operators
@multi_binary_operators [:..//]
@multi_binary_operators [:"..//"]
# Operators that do not have space between operands
@no_space_binary_operators [:.., :"//"]
@@ -31,37 +31,30 @@ defmodule Code.Formatter do
@right_new_line_before_binary_operators [:|, :when]
# Operators that are logical cannot be mixed without parens
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
@required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
# Operators with next break fits
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
# Operators that always require parens even
# when they are their own parents as they are not semantically associative
@required_parens_even_when_parent [:--, :---]
# Operators that always require parens on operands
# when they are the parent of another operator with a difference precedence
# Most operators are listed, except comparison, arithmetic, and low precedence
# Operators that always require parens on operands when they are the parent
@required_parens_on_binary_operands [
:|>,
:<<<,
:>>>,
:|>,
:<~,
:~>,
:<<~,
:~>>,
:<~>,
:"<|>",
:in,
:"^^^",
:"//",
:++,
:--,
:+++,
:---,
:<>,
:..
:in,
:++,
:--,
:..,
:<>
]
@locals_without_parens [
@@ -189,10 +182,8 @@ defmodule Code.Formatter do
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
syntax_colors = Keyword.get(opts, :syntax_colors, [])
sigils =
@@ -217,9 +208,8 @@ defmodule Code.Formatter do
comments: comments,
sigils: sigils,
file: file,
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
migrate_unless: migrate_unless,
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
}
end
@@ -276,7 +266,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
{var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
end
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
@@ -407,7 +397,7 @@ defmodule Code.Formatter do
{@double_heredoc
|> concat(string)
|> concat(@double_heredoc)
|> color_doc(:string, state.inspect_opts)
|> color(:string, state.inspect_opts)
|> force_unfit(), state}
else
string = escape_string(string, @double_quote)
@@ -415,7 +405,7 @@ defmodule Code.Formatter do
{@double_quote
|> concat(string)
|> concat(@double_quote)
|> color_doc(:string, state.inspect_opts), state}
|> color(:string, state.inspect_opts), state}
end
end
@@ -447,7 +437,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{color_doc("nil", nil, state.inspect_opts), state}
{color("nil", nil, state.inspect_opts), state}
end
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
@@ -464,7 +454,7 @@ defmodule Code.Formatter do
end
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|> color_doc(:atom, state.inspect_opts), state}
|> color(:atom, state.inspect_opts), state}
end
# &1
@@ -487,53 +477,6 @@ defmodule Code.Formatter do
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
end
# disable migrate_unless within defmacro
defp quoted_to_algebra(
{atom, _, [{:unless, _, _}, _]} = ast,
context,
%{migrate_unless: true} = state
)
when atom in [:defmacro, :defmacrop] do
quoted_to_algebra(ast, context, %{state | migrate_unless: false})
end
# rewrite unless as if!
defp quoted_to_algebra(
{:unless, meta, [condition, block]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
end
# a |> b() |> unless(...) => a |> b() |> Kernel.!() |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [{:|>, _, _} = condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
negated_condition = {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
quoted_to_algebra(
{:|>, meta1, [negated_condition, {:if, meta2, [block]}]},
context,
state
)
end
# condition |> unless(...) => negated(condition) |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra(
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
context,
state
)
end
# ..
defp quoted_to_algebra({:.., _meta, []}, context, state) do
if context in [:no_parens_arg, :no_parens_one_arg] do
@@ -549,7 +492,7 @@ defmodule Code.Formatter do
end
# 1..2//3
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
end
@@ -581,12 +524,20 @@ defmodule Code.Formatter do
if keyword_key?(left_arg) do
{left, state} =
case left_arg do
{:__block__, _, [atom]} when is_atom(atom) ->
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
{:__block__, _, [:"..//"]} ->
{string(~S{"..//":}), state}
{formatted
|> string()
|> color_doc(:atom, state.inspect_opts), state}
{:__block__, _, [atom]} when is_atom(atom) ->
iodata =
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
[Atom.to_string(atom), ?:]
else
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
end
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
state}
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
@@ -610,11 +561,8 @@ defmodule Code.Formatter do
end
# #PID's and #Ref's may appear on regular AST
# Other foreign structures, such as maps and structs,
# may appear from Macro.to_string, so we stick a limit,
# although they won't be formatted accordingly.
defp quoted_to_algebra(unknown, _context, state) do
{inspect(unknown, printable_limit: :infinity), state}
{inspect(unknown), state}
end
## Blocks
@@ -684,7 +632,7 @@ defmodule Code.Formatter do
Atom.to_string(op)
end
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
end
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
@@ -790,11 +738,11 @@ defmodule Code.Formatter do
doc =
cond do
op in @no_space_binary_operators ->
op_doc = color_doc(op_string, :operator, state.inspect_opts)
op_doc = color(op_string, :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
op in @no_newline_binary_operators ->
op_doc = color_doc(" " <> op_string <> " ", :operator, state.inspect_opts)
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
true ->
@@ -804,7 +752,7 @@ defmodule Code.Formatter do
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
with_next_break_fits(next_break_fits?, right, fn right ->
op_doc = color_doc(" " <> op_string, :operator, state.inspect_opts)
op_doc = color(" " <> op_string, :operator, state.inspect_opts)
right = nest(glue(op_doc, group(right)), nesting, :break)
right = if eol?, do: force_unfit(right), else: right
concat(group(left), group(right))
@@ -841,13 +789,14 @@ defmodule Code.Formatter do
op_string = Atom.to_string(op)
cond do
# If we have the same operator and it is in the correct side,
# we don't add parens unless it is explicitly required.
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent ->
# If the operator has the same precedence as the parent and is on
# the correct side, we respect the nesting rule to avoid multiple
# nestings. This only applies for left associativity or same operator.
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
# If the operator requires parens (most of them do) or we are mixing logical operators
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis.
# If the parent requires parens or the precedence is inverted or
# it is in the wrong side, then we *need* parenthesis.
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
(op in @required_parens_logical_binary_operands and
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
@@ -1004,7 +953,7 @@ defmodule Code.Formatter do
)
when is_atom(fun) and is_integer(arity) do
{target_doc, state} = remote_target_to_algebra(target, state)
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
fun = Macro.inspect_atom(:remote_call, fun)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end
@@ -1051,9 +1000,7 @@ defmodule Code.Formatter do
{target_doc, state} = remote_target_to_algebra(target, state)
fun_doc =
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|> string()
|> color_doc(:call, state.inspect_opts)
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
@@ -1121,7 +1068,7 @@ defmodule Code.Formatter do
fun
|> Atom.to_string()
|> string()
|> color_doc(:call, state.inspect_opts)
|> color(:call, state.inspect_opts)
|> concat(call_doc)
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
@@ -1484,7 +1431,7 @@ defmodule Code.Formatter do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} =
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
spec = wrap_in_parens_if_inspected_atom(spec)
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
@@ -1562,8 +1509,8 @@ defmodule Code.Formatter do
{args_doc, _join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("[", :list, state.inspect_opts)
right_bracket = color_doc("]", :list, state.inspect_opts)
left_bracket = color("[", :list, state.inspect_opts)
right_bracket = color("]", :list, state.inspect_opts)
{surround(left_bracket, args_doc, right_bracket), state}
end
@@ -1595,8 +1542,8 @@ defmodule Code.Formatter do
end
defp do_map_to_algebra(name_doc, args_doc, state) do
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
end
defp tuple_to_algebra(meta, args, join, state) do
@@ -1606,8 +1553,8 @@ defmodule Code.Formatter do
{args_doc, join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("{", :tuple, state.inspect_opts)
right_bracket = color_doc("}", :tuple, state.inspect_opts)
left_bracket = color("{", :tuple, state.inspect_opts)
right_bracket = color("}", :tuple, state.inspect_opts)
if join == :flex_break do
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
@@ -1617,11 +1564,16 @@ defmodule Code.Formatter do
end
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
Atom.to_string(atom) |> color(:boolean, inspect_opts)
end
defp atom_to_algebra(nil, _, inspect_opts) do
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
Atom.to_string(nil) |> color(nil, inspect_opts)
end
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
defp atom_to_algebra(:"..//", _, inspect_opts) do
string(":\"..//\"") |> color(:atom, inspect_opts)
end
defp atom_to_algebra(:\\, meta, inspect_opts) do
@@ -1634,7 +1586,7 @@ defmodule Code.Formatter do
_ -> ":\\\\"
end
string(string) |> color_doc(:atom, inspect_opts)
string(string) |> color(:atom, inspect_opts)
end
defp atom_to_algebra(atom, _, inspect_opts) do
@@ -1647,7 +1599,7 @@ defmodule Code.Formatter do
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
end
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
end
defp integer_to_algebra(text, inspect_otps) do
@@ -1664,7 +1616,7 @@ defmodule Code.Formatter do
decimal ->
insert_underscores(decimal)
end
|> color_doc(:number, inspect_otps)
|> color(:number, inspect_otps)
end
defp float_to_algebra(text, inspect_otps) do
@@ -1672,7 +1624,7 @@ defmodule Code.Formatter do
decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_otps)
color(string, :number, inspect_otps)
end
defp insert_underscores("-" <> digits) do
@@ -2437,10 +2389,6 @@ defmodule Code.Formatter do
meta[:closing][:line] || @min_line
end
defp escape_atom(string, char) do
String.replace(string, <<char>>, <<?\\, char>>)
end
## Algebra helpers
# Relying on the inner document is brittle and error prone.
@@ -2486,7 +2434,7 @@ defmodule Code.Formatter do
end
defp get_charlist_quotes(:heredoc, state) do
if state.migrate_charlists_as_sigils do
if state.normalize_charlists_as_sigils do
{@sigil_c_heredoc, @double_heredoc}
else
{@single_heredoc, @single_heredoc}
@@ -2495,7 +2443,7 @@ defmodule Code.Formatter do
defp get_charlist_quotes({:regular, chunks}, state) do
cond do
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
!state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
true -> {@sigil_c_double, @double_quote}
end
@@ -2504,46 +2452,4 @@ defmodule Code.Formatter do
defp has_double_quote?(chunk) do
is_binary(chunk) and chunk =~ @double_quote
end
# Migration rewrites
@bool_operators [
:>,
:>=,
:<,
:<=,
:in
]
@guards [
:is_atom,
:is_boolean,
:is_nil,
:is_number,
:is_integer,
:is_float,
:is_binary,
:is_map,
:is_struct,
:is_non_struct_map,
:is_exception,
:is_list,
:is_tuple,
:is_function,
:is_reference,
:is_pid,
:is_port
]
defp negate_condition(condition) do
case condition do
{neg, _, [condition]} when neg in [:!, :not] -> condition
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
_ -> {:!, [], [condition]}
end
end
end
+28 -144
View File
@@ -320,9 +320,6 @@ defmodule Code.Fragment do
_ ->
{{:local_or_var, acc}, count}
end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end
end
@@ -365,14 +362,6 @@ defmodule Code.Fragment do
:none
end
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
if Enum.all?(acc, &(&1 in ?0..?9)) do
{:capture_arg, [?& | acc], count + 1}
else
tokenize_identifier(rest, count, acc)
end
end
defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc)
end
@@ -573,13 +562,13 @@ defmodule Code.Fragment do
`local_call`/`local_arity` and `local_or_var`, since the latter can
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`
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
* This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ''}` as context
* This function returns keywords as `{:keyword, ~c"do"}`
* This function returns keywords as `{:keyword, 'do'}`
* This function never returns `:expr`
@@ -601,9 +590,7 @@ defmodule Code.Fragment do
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
| {:keyword, charlist}
| {:key, charlist}
| {:capture_arg, charlist},
| {:keyword, charlist},
inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
@@ -653,16 +640,7 @@ defmodule Code.Fragment do
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{keyword_key?, rest} =
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
{rest, _} = strip_spaces(rest, 0)
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
@@ -678,9 +656,6 @@ defmodule Code.Fragment do
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when keyword_key? ->
build_surround({:key, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset)
@@ -708,9 +683,6 @@ defmodule Code.Fragment do
{{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset)
{{:capture_arg, acc}, offset} ->
build_surround({:capture_arg, acc}, reversed, line, offset)
_ ->
maybe_operator(reversed_pre, post, line, opts)
end
@@ -743,16 +715,6 @@ defmodule Code.Fragment do
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
arg = Enum.take_while(rest, &(&1 in ?0..?9))
build_surround(
{:capture_arg, ~c"&" ++ arg},
:lists.reverse(arg, reversed),
line,
offset + length(arg)
)
{{:operator, acc}, offset} ->
build_surround({:operator, acc}, reversed, line, offset)
@@ -1099,13 +1061,6 @@ defmodule Code.Fragment do
iex> Code.Fragment.container_cursor_to_quoted("foo +")
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
In order to parse the left-side of `->` properly, which appears both
in anonymous functions and do-end blocks, the trailing fragment option
must be given with the rest of the contents:
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
## Options
* `:file` - the filename to be reported in case of parsing errors.
@@ -1128,117 +1083,46 @@ defmodule Code.Fragment do
* `:literal_encoder` - a function to encode literals in the AST.
See the documentation for `Code.string_to_quoted/2` for more information.
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
{: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)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
opts = [cursor_completion: true, emit_warnings: false] ++ opts
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
when trailing_fragment == nil ->
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
_ -> {rev_tokens, rev_terminators}
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
with {before_start, [_ | _] = after_start} <-
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, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
tokens = :lists.reverse(rev_tokens, rev_terminators)
# If the cursor has its own line, then we do not trim new lines trailing tokens.
# Otherwise we want to drop any newline so we drop the next tokens after eol.
trailing_tokens =
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, ast} ->
{:ok, ast}
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
{:error, error} ->
# In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
tokens =
:lists.reverse(
rev_tokens,
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
Enum.map(rev_terminators, fn tuple ->
{line, column, info} = elem(tuple, 1)
put_elem(tuple, 1, {line, column + 5, info})
end)
)
:elixir.tokens_to_quoted(tokens, file, opts)
case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, ast} -> {:ok, ast}
{:error, _} -> {:error, error}
end
end
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
end
defp reverse_tokens(line, column, tokens, terminators) do
{terminators, _} =
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
atom = :elixir_tokenizer.terminator(start)
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
end)
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end
+29 -61
View File
@@ -71,11 +71,11 @@ defmodule Code.Identifier do
Escapes the given identifier.
"""
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: binary(), remaining :: binary()}
{escaped :: iolist(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
(is_integer(limit) and limit >= 0) do
escape(binary, char, limit, <<>>, fun)
escape(binary, char, limit, [], fun)
end
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
@@ -83,96 +83,64 @@ defmodule Code.Identifier do
end
defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
if value = fun.(h) do
value = IO.iodata_to_binary(value)
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
else
escape(t, char, decrement(count), escape_char(h, acc), fun)
end
escaped = if value = fun.(h), do: value, else: escape_char(h)
escape(t, char, decrement(count), [acc | escaped], fun)
end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
end
defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>}
end
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
defp escape_char(0), do: [?\\, ?0]
defp escape_char(char, acc)
# Some characters that are confusing (zero-width / alternative spaces) are displayed
# using their unicode representation:
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
@escaped_bom :binary.bin_to_list("\\uFEFF")
defp escape_char(65279), do: @escaped_bom
# BOM
when char == 0xFEFF
# Mathematical invisibles
when char in 0x2061..0x2064
# Bidirectional neutral
when char in [0x061C, 0x200E, 0x200F]
# Bidirectional general (source of vulnerabilities)
when char in 0x202A..0x202E
when char in 0x2066..0x2069
# Interlinear annotations
when char in 0xFFF9..0xFFFC
# Zero-width joiners and non-joiners
when char in [0x200C, 0x200D, 0x034F]
# Non-break space / zero-width space
when char in [0x00A0, 0x200B, 0x2060]
# Line/paragraph separators
when char in [0x2028, 0x2029]
# Spaces
when char in 0x2000..0x200A
when char == 0x205F 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)>>
end
defp escape_char(char, acc)
defp escape_char(char)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
<<acc::binary, char::utf8>>
<<char::utf8>>
end
defp escape_char(char, acc) when char < 0x100 do
defp escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
[?\\, ?x, to_hex(a), to_hex(b)]
end
defp escape_char(char, acc) when char < 0x10000 do
defp escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
end
defp escape_char(char, acc) when char < 0x1000000 do
defp escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
end
defp escape_map(?\a), do: "\\a"
defp escape_map(?\b), do: "\\b"
defp escape_map(?\d), do: "\\d"
defp escape_map(?\e), do: "\\e"
defp escape_map(?\f), do: "\\f"
defp escape_map(?\n), do: "\\n"
defp escape_map(?\r), do: "\\r"
defp escape_map(?\t), do: "\\t"
defp escape_map(?\v), do: "\\v"
defp escape_map(?\\), do: "\\\\"
defp escape_map(?\a), do: [?\\, ?a]
defp escape_map(?\b), do: [?\\, ?b]
defp escape_map(?\d), do: [?\\, ?d]
defp escape_map(?\e), do: [?\\, ?e]
defp escape_map(?\f), do: [?\\, ?f]
defp escape_map(?\n), do: [?\\, ?n]
defp escape_map(?\r), do: [?\\, ?r]
defp escape_map(?\t), do: [?\\, ?t]
defp escape_map(?\v), do: [?\\, ?v]
defp escape_map(?\\), do: [?\\, ?\\]
defp escape_map(_), do: false
@compile {:inline, to_hex: 1, decrement: 1}
+21 -33
View File
@@ -58,7 +58,7 @@ defmodule Code.Normalizer do
{:.., meta, [left, right]}
else
step = do_normalize(step, state)
{:..//, meta, [left, right, step]}
{:"..//", meta, [left, right, step]}
end
end
@@ -126,13 +126,13 @@ defmodule Code.Normalizer do
{:., meta, [Access, :get]}
end
# Only normalize the left side of the dot operator
# The right hand side is an atom in the AST but it's not an atom literal, so
# it should not be wrapped. However, it should be escaped if applicable.
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
# it should not be wrapped
defp do_normalize({:., meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
left = do_normalize(left, %{state | parent_meta: meta})
right = maybe_escape_literal(right, state)
{:., meta, [left, right]}
end
@@ -289,18 +289,21 @@ defmodule Code.Normalizer do
# Lists
defp normalize_literal(list, meta, state) when is_list(list) do
if list != [] and List.ascii_printable?(list) do
# It's a charlist, we normalize it as a ~C sigil
string =
# It's a charlist
list =
if state.escape do
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(iolist)
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(string) |> to_charlist()
else
List.to_string(list)
list
end
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
{:__block__, meta, [list]}
else
meta =
if line = state.parent_meta[:line] do
@@ -347,14 +350,14 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
Keyword.has_key?(meta, :do) ->
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
# Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line]
line = state.parent_meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
@@ -364,20 +367,11 @@ defmodule Code.Normalizer do
last_args =
case last_arg do
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} ->
cond do
key_meta[:format] == :keyword ->
[keyword]
block_keyword?(keyword) ->
[
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
{{:__block__, [format: :keyword] ++ meta, args}, value}
end)
]
true ->
[last_arg]
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
if key_meta[:format] == :keyword do
last_args
else
[last_arg]
end
[] ->
@@ -391,12 +385,6 @@ defmodule Code.Normalizer do
end
end
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
do: block_keyword?(tail)
defp block_keyword?([]), do: true
defp block_keyword?(_), 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)
+11 -5
View File
@@ -27,6 +27,11 @@ defmodule Code.Typespec do
end
end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -283,6 +288,7 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
@@ -311,6 +317,10 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end
@@ -328,14 +338,10 @@ defmodule Code.Typespec do
{erl_to_ex_var(var), meta(anno), nil}
end
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do
defp typespec_to_quoted({:op, anno, op, arg}) do
{op, meta(anno), [typespec_to_quoted(arg)]}
end
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args)
end
+2 -28
View File
@@ -41,8 +41,7 @@ defmodule Config do
> application environment is effectively a global storage. Also note that
> the `config/config.exs` of a library is not evaluated when the library is
> used as a dependency, as configuration is always meant to configure the
> current project. For more information, see ["Using application configuration for
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
> current project. For more information, read our [library guidelines](library-guidelines.md).
## Migrating from `use Mix.Config`
@@ -144,7 +143,7 @@ defmodule Config do
"""
@doc since: "1.9.0"
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end
@@ -195,31 +194,6 @@ defmodule Config do
|> put_config()
end
@doc """
Reads the configuration for the given root key.
This function only reads the configuration from a previous
`config/2` or `config/3` call. If `root_key` points to an
application, it does not read its actual application environment.
Its main use case is to make it easier to access and share
configuration values across files.
If the `root_key` was not configured, it returns `nil`.
## Examples
# In config/config.exs
config :my_app, foo: :bar
# In config/dev.exs
config :another_app, foo: read_config(:my_app)[:foo] || raise "missing parent configuration"
"""
@doc since: "1.18.0"
def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key]
end
@doc """
Returns the environment this configuration file is executed on.
+7 -12
View File
@@ -215,7 +215,10 @@ defmodule DynamicSupervisor do
extra_arguments: [term()]
}
@typedoc "Options given to `start_link/1` and `init/1` functions"
@typedoc "Options given to `start_link` functions"
@type option :: GenServer.option()
@typedoc "Options given to `start_link` and `init/1` functions"
@type init_option ::
{:strategy, strategy()}
| {:max_restarts, non_neg_integer()}
@@ -274,7 +277,7 @@ defmodule DynamicSupervisor do
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour DynamicSupervisor
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -342,11 +345,9 @@ defmodule DynamicSupervisor do
specified in the child spec given to `start_child/2`. Defaults to
an empty list.
* Any of the standard [GenServer options](`t:GenServer.option/0`)
"""
@doc since: "1.6.0"
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start()
@spec start_link([option | init_option]) :: Supervisor.on_start()
def start_link(options) when is_list(options) do
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
{sup_opts, start_opts} = Keyword.split(options, keys)
@@ -380,15 +381,9 @@ defmodule DynamicSupervisor do
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
with `:normal` reason.
## Options
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
callback.
"""
@doc since: "1.6.0"
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
@spec start_link(module, term, [option]) :: Supervisor.on_start()
def start_link(module, init_arg, opts \\ []) do
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
end
+31 -141
View File
@@ -837,7 +837,7 @@ defmodule Enum do
end
def dedup(enumerable) do
reduce(enumerable, [], fn x, acc ->
Enum.reduce(enumerable, [], fn x, acc ->
case acc do
[^x | _] -> acc
_ -> [x | acc]
@@ -1257,14 +1257,6 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[:a], [:b], [:c]]
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 ->
...> if x != 0, do: [1 / x], else: []
...> end)
[0.25, 0.5]
"""
@spec flat_map(t, (element -> t)) :: list
def flat_map(enumerable, fun) when is_list(enumerable) do
@@ -1274,7 +1266,6 @@ defmodule Enum do
def flat_map(enumerable, fun) do
reduce(enumerable, [], fn entry, acc ->
case fun.(entry) do
[] -> acc
list when is_list(list) -> [list | acc]
other -> [to_list(other) | acc]
end
@@ -1282,8 +1273,6 @@ defmodule Enum do
|> flat_reverse([])
end
# the first clause is an optimization
defp flat_reverse([[elem] | t], acc), do: flat_reverse(t, [elem | acc])
defp flat_reverse([h | t], acc), do: flat_reverse(t, h ++ acc)
defp flat_reverse([], acc), do: acc
@@ -1549,7 +1538,7 @@ defmodule Enum do
do: Map.merge(collectable, :maps.from_list(enumerable))
defp into_map(enumerable, collectable),
do: reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
do: Enum.reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
defp into_protocol(enumerable, collectable) do
{initial, fun} = Collectable.into(collectable)
@@ -1592,7 +1581,7 @@ defmodule Enum do
"""
@spec into(Enumerable.t(), Collectable.t(), (term -> term)) :: Collectable.t()
def into(enumerable, [], transform) do
map(enumerable, transform)
Enum.map(enumerable, transform)
end
def into(%_{} = enumerable, collectable, transform) do
@@ -1605,9 +1594,9 @@ defmodule Enum do
def into(enumerable, %{} = collectable, transform) do
if map_size(collectable) == 0 do
enumerable |> map(transform) |> :maps.from_list()
enumerable |> Enum.map(transform) |> :maps.from_list()
else
reduce(enumerable, collectable, fn entry, acc ->
Enum.reduce(enumerable, collectable, fn entry, acc ->
{key, val} = transform.(entry)
Map.put(acc, key, val)
end)
@@ -2427,17 +2416,13 @@ defmodule Enum do
{:ok, count, fun} when is_function(fun, 1) ->
slice_list(fun.(enumerable), random_count(count), 1, 1)
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
fun.(random_count(count), 1)
{:ok, count, fun} when is_function(fun, 3) ->
fun.(random_count(count), 1, 1)
# TODO: Remove deprecation on Elixir v1.20.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
fun.(random_count(count), 1)
{:error, _} ->
take_random(enumerable, 1)
end
@@ -2717,11 +2702,11 @@ defmodule Enum do
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1)
[:a, :f, :b, :c, :d, :e, :g]
# Slide a range of elements towards the head of the list
# Slide a range of elements backward
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..5, 1)
[:a, :d, :e, :f, :b, :c, :g]
# Slide a range of elements towards the tail of the list
# Slide a range of elements forward
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5)
[:a, :e, :f, :b, :c, :d, :g]
@@ -2808,7 +2793,7 @@ defmodule Enum do
# Then at the end, we're going to reassemble and reverse them, and end up with the
# chunks in the correct order.
{_size, pre, post} =
reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
{pre, post} =
cond do
index < start -> {[item | pre], post}
@@ -3353,7 +3338,7 @@ defmodule Enum do
def sort_by(enumerable, mapper, :desc) when is_function(mapper, 1) do
enumerable
|> reduce([], &[{&1, mapper.(&1)} | &2])
|> Enum.reduce([], &[{&1, mapper.(&1)} | &2])
|> List.keysort(1, :asc)
|> List.foldl([], &[elem(&1, 0) | &2])
end
@@ -3458,8 +3443,6 @@ defmodule Enum do
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
If you need to apply a transformation first, consider using `Enum.sum_by/2` instead.
## Examples
iex> Enum.sum([1, 2, 3])
@@ -3486,47 +3469,11 @@ defmodule Enum do
reduce(enumerable, 0, &+/2)
end
@doc """
Maps and sums the given `enumerable` in one pass.
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
## Examples
iex> Enum.sum_by([%{count: 1}, %{count: 2}, %{count: 3}], fn x -> x.count end)
6
iex> Enum.sum_by(1..3, fn x -> x ** 2 end)
14
iex> Enum.sum_by([], fn x -> x.count end)
0
Filtering can be achieved by returning `0` to ignore elements:
iex> Enum.sum_by([1, -2, 3], fn x -> if x > 0, do: x, else: 0 end)
4
"""
@doc since: "1.18.0"
@spec sum_by(t, (element -> number)) :: number
def sum_by(enumerable, mapper)
def sum_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
sum_by_list(list, mapper, 0)
end
def sum_by(enumerable, mapper) when is_function(mapper, 1) do
reduce(enumerable, 0, fn x, acc -> acc + mapper.(x) end)
end
@doc """
Returns the product of all elements.
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
If you need to apply a transformation first, consider using `Enum.product_by/2` instead.
## Examples
iex> Enum.product([])
@@ -3543,40 +3490,6 @@ defmodule Enum do
reduce(enumerable, 1, &*/2)
end
@doc """
Maps and computes the product of the given `enumerable` in one pass.
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
## Examples
iex> Enum.product_by([%{count: 2}, %{count: 4}, %{count: 3}], fn x -> x.count end)
24
iex> Enum.product_by(1..3, fn x -> x ** 2 end)
36
iex> Enum.product_by([], fn x -> x.count end)
1
Filtering can be achieved by returning `1` to ignore elements:
iex> Enum.product_by([2, -1, 3], fn x -> if x > 0, do: x, else: 1 end)
6
"""
@doc since: "1.18.0"
@spec product_by(t, (element -> number)) :: number
def product_by(enumerable, mapper)
def product_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
product_by_list(list, mapper, 1)
end
def product_by(enumerable, mapper) when is_function(mapper, 1) do
reduce(enumerable, 1, fn x, acc -> acc * mapper.(x) end)
end
@doc """
Takes an `amount` of elements from the beginning or the end of the `enumerable`.
@@ -3837,9 +3750,6 @@ defmodule Enum do
@doc """
Enumerates the `enumerable`, removing all duplicate elements.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
## Examples
iex> Enum.uniq([1, 2, 3, 3, 2, 1])
@@ -3865,8 +3775,7 @@ defmodule Enum do
considered duplicates if the return value of `fun` is equal for
both of them.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
The first occurrence of each element is kept.
## Example
@@ -3936,11 +3845,13 @@ defmodule Enum do
Returns the `enumerable` with each element wrapped in a tuple
alongside its index or according to a given function.
If an integer offset is given as `fun_or_offset`, it will index from the given
offset instead of from zero.
May receive a function or an integer offset.
If a function is given as `fun_or_offset`, it will index by invoking the function
for each element and index (zero-based) of the enumerable.
If an `offset` is given, it will index from the given offset instead of from
zero.
If a `function` is given, it will index by invoking the function for each
element and index (zero-based) of the enumerable.
## Examples
@@ -4140,7 +4051,7 @@ defmodule Enum do
def zip_reduce(left, right, acc, reducer) when is_function(reducer, 3) do
reduce = fn [l, r], acc -> {:cont, reducer.(l, r, acc)} end
R.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
Stream.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
end
@doc """
@@ -4175,7 +4086,7 @@ defmodule Enum do
def zip_reduce([], acc, reducer) when is_function(reducer, 2), do: acc
def zip_reduce(enums, acc, reducer) when is_function(reducer, 2) do
R.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
Stream.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
end
## Helpers
@@ -4440,9 +4351,6 @@ defmodule Enum do
defp flat_map_list([head | tail], fun) do
case fun.(head) do
# the two first clauses are an optimization
[] -> flat_map_list(tail, fun)
[elem] -> [elem | flat_map_list(tail, fun)]
list when is_list(list) -> list ++ flat_map_list(tail, fun)
other -> to_list(other) ++ flat_map_list(tail, fun)
end
@@ -4584,16 +4492,8 @@ defmodule Enum do
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
enumerable |> fun.() |> slice_exact(start, amount, step, count)
{:ok, count, fun} when is_function(fun, 3) ->
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
fun.(start, amount, step)
# TODO: Remove me on v2.0.
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
amount = Kernel.min(amount, count - start)
if step == 1 do
@@ -4603,6 +4503,10 @@ defmodule Enum do
|> take_every_list(amount, step - 1)
end
{:ok, count, fun} when is_function(fun, 3) ->
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
fun.(start, amount, step)
{:error, module} ->
slice_enum(enumerable, module, start, amount, step)
end
@@ -4661,18 +4565,11 @@ defmodule Enum do
defp slice_count_and_fun(enumerable, step) do
case Enumerable.slice(enumerable) do
{:ok, count, fun} when is_function(fun, 1) ->
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
{:ok, count, fun} when is_function(fun, 3) ->
{count, fun}
# TODO: Remove me on v2.0
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
if step == 1 do
{count, fn start, amount, 1 -> fun.(start, amount) end}
else
@@ -4683,6 +4580,9 @@ defmodule Enum do
end}
end
{:ok, count, fun} when is_function(fun, 1) ->
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
{:error, module} ->
{list, count} =
enumerable
@@ -4858,16 +4758,6 @@ defmodule Enum do
{:lists.reverse(acc), []}
end
## sum_by
defp sum_by_list([], _, acc), do: acc
defp sum_by_list([h | t], mapper, acc), do: sum_by_list(t, mapper, acc + mapper.(h))
## product_by
defp product_by_list([], _, acc), do: acc
defp product_by_list([h | t], mapper, acc), do: product_by_list(t, mapper, acc * mapper.(h))
## take
defp take_list(_list, 0), do: []
+53 -130
View File
@@ -49,7 +49,7 @@ defmodule Exception do
@doc """
Receives the exception struct and must return its message.
Many exceptions have a message field which by default is accessed
Most commonly exceptions have a message field which by default is accessed
by this function. However, if an exception does not have a message field,
this function must be explicitly implemented.
"""
@@ -346,7 +346,7 @@ defmodule Exception do
defp rewrite_arg(arg) do
Macro.prewalk(arg, fn
{:%{}, meta, [__struct__: Range, first: first, last: last, step: step]} ->
{:..//, meta, [first, last, step]}
{:"..//", meta, [first, last, step]}
other ->
other
@@ -407,8 +407,8 @@ defmodule Exception do
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args, _} -> {fun, meta, args}
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
@@ -1581,15 +1581,13 @@ defmodule UndefinedFunctionError do
"""
@function_threshold 0.77
@max_suggestions 5
defexception [:module, :function, :arity, :reason, :message]
@impl true
def message(%{message: nil} = exception) do
%{reason: reason, module: module, function: function, arity: arity} = exception
{message, hint_type} = message(reason, module, function, arity)
message <> if(hint_type == :suggest_module, do: hint_for_missing_alias(module), else: "")
{message, _hint_type} = message(reason, module, function, arity)
message
end
def message(%{message: message}) do
@@ -1650,25 +1648,12 @@ defmodule UndefinedFunctionError do
end
defp hint(module, function, arity, :suggest_function) do
hint_for_behaviour(module, function, arity) <>
hint_for_loaded_module(module, function, arity)
behaviour_hint(module, function, arity) <>
hint_for_loaded_module(module, function, arity, nil)
end
@max_suggestions 5
defp hint(module, function, arity, :suggest_module) do
hint_for_missing_module(module, function, arity)
end
defp hint_for_missing_alias(module) do
with "Elixir." <> rest <- Atom.to_string(module),
false <- rest =~ "." do
". Make sure the module name is correct and has been specified in full (or that an alias has been defined)"
else
_ -> ""
end
end
@doc false
def hint_for_missing_module(module, function, arity) do
downcased_module = downcase_module_name(module)
stripped_module = module |> Atom.to_string() |> String.replace_leading("Elixir.", "")
@@ -1689,9 +1674,9 @@ defmodule UndefinedFunctionError do
["\n * ", Exception.format_mfa(module, function, arity)]
end)
". Did you mean:\n#{suggestions}\n"
IO.iodata_to_binary([". Did you mean:\n", suggestions, "\n"])
else
hint_for_missing_alias(module)
""
end
end
@@ -1708,7 +1693,7 @@ defmodule UndefinedFunctionError do
end
@doc false
def hint_for_loaded_module(module, function, arity) do
def hint_for_loaded_module(module, function, arity, exports) do
cond do
macro_exported?(module, function, arity) ->
". However, there is a macro with the same name and arity. " <>
@@ -1718,7 +1703,7 @@ defmodule UndefinedFunctionError do
", #{message}"
true ->
IO.iodata_to_binary(did_you_mean(module, function))
IO.iodata_to_binary(did_you_mean(module, function, exports))
end
end
@@ -1729,39 +1714,11 @@ defmodule UndefinedFunctionError do
end
end
defp hint_for_behaviour(module, function, arity) do
case behaviours_for(module) do
[] ->
""
@function_threshold 0.77
@max_suggestions 5
behaviours ->
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
nil -> ""
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
end
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> ""
end
defp behaviours_for(module) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
end
defp expects_callback?(behaviour, function, arity) do
callbacks =
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
Enum.member?(callbacks, {function, arity})
end
## Shared helpers across hints
defp did_you_mean(module, function) do
exports = exports_for(module)
defp did_you_mean(module, function, exports) do
exports = exports || exports_for(module)
result =
case Keyword.take(exports, [function]) do
@@ -1791,6 +1748,35 @@ defmodule UndefinedFunctionError do
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp behaviour_hint(module, function, arity) do
case behaviours_for(module) do
[] ->
""
behaviours ->
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
nil -> ""
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
end
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> ""
end
defp behaviours_for(module) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
end
defp expects_callback?(behaviour, function, arity) do
callbacks =
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
Enum.member?(callbacks, {function, arity})
end
defp exports_for(module) do
if function_exported?(module, :__info__, 1) do
module.__info__(:macros) ++ module.__info__(:functions)
@@ -1799,7 +1785,8 @@ defmodule UndefinedFunctionError do
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> []
UndefinedFunctionError ->
[]
end
defp deprecated_functions_for(module) do
@@ -1810,7 +1797,8 @@ defmodule UndefinedFunctionError do
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> []
UndefinedFunctionError ->
[]
end
end
@@ -1997,23 +1985,8 @@ defmodule Protocol.UndefinedError do
@impl true
def message(%{protocol: protocol, value: value, description: description}) do
inspected =
value
|> inspect(pretty: true)
# Indent only lines with contents on them
|> String.replace(~r/^(?=.+)/m, " ")
"protocol #{inspect(protocol)} not implemented for type " <>
value_type(value) <>
maybe_description(description) <>
maybe_available(protocol) <>
"""
Got value:
#{inspected}
"""
"protocol #{inspect(protocol)} not implemented for #{inspect(value)} of type " <>
value_type(value) <> maybe_description(description) <> maybe_available(protocol)
end
defp value_type(%{__struct__: struct}), do: "#{inspect(struct)} (a struct)"
@@ -2158,56 +2131,6 @@ defmodule UnicodeConversionError do
end
end
defmodule MissingApplicationsError do
@moduledoc """
An exception that is raised when an application depends on one or more
missing applications.
This exception is used by Mix and other tools. It can also be used by library authors
when their library only requires an external application (like a dependency) for a subset
of features.
The fields of this exception are public. See `t:t/0`.
*Available since v1.18.0.*
## Examples
unless Application.spec(:plug, :vsn) do
raise MissingApplicationsError,
description: "application :plug is required for testing Plug-related functionality",
apps: [{:plug, "~> 1.0"}]
end
"""
@moduledoc since: "1.18.0"
@type t() :: %__MODULE__{
apps: [{Application.app(), Version.requirement()}, ...],
description: String.t()
}
defexception apps: [], description: "missing applications found"
@impl true
def message(%__MODULE__{apps: apps, description: description}) do
# We explicitly format these as tuples so that they're easier to copy-paste
# into dependencies.
formatted_apps =
Enum.map(apps, fn {app_name, requirement} ->
~s(\n {#{inspect(app_name)}, "#{requirement}"})
end)
"""
#{description}
To address this, include these applications as your dependencies:
#{formatted_apps}\
"""
end
end
defmodule Enum.OutOfBoundsError do
@moduledoc """
An exception that is raised when a function expects an enumerable to have
@@ -2530,7 +2453,7 @@ defmodule ErlangError do
{:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])}
general = extra[:general] ->
{:ok, reason, ": " <> IO.chardata_to_string(general)}
{:ok, reason, ": " <> general}
true ->
:error
+11 -11
View File
@@ -139,8 +139,6 @@ defmodule Float do
iex> Float.parse("56.5xyz")
{56.5, "xyz"}
iex> Float.parse(".12")
:error
iex> Float.parse("pi")
:error
iex> Float.parse("1.7976931348623159e+308")
@@ -577,14 +575,18 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation
of the given float.
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
Design and Implementation.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`.
Inlined by the compiler.
## Examples
iex> Float.to_charlist(7.0)
@@ -592,7 +594,7 @@ defmodule Float do
"""
@spec to_charlist(float) :: charlist
def to_charlist(float) do
def to_charlist(float) when is_float(float) do
:erlang.float_to_list(float, [:short])
end
@@ -612,8 +614,6 @@ defmodule Float do
For a configurable representation, use `:erlang.float_to_binary/2`.
Inlined by the compiler.
## Examples
iex> Float.to_string(7.0)
@@ -621,7 +621,7 @@ defmodule Float do
"""
@spec to_string(float) :: String.t()
def to_string(float) do
def to_string(float) when is_float(float) do
:erlang.float_to_binary(float, [:short])
end
+2 -2
View File
@@ -46,8 +46,8 @@ defmodule GenEvent do
@callback init(args :: term) ::
{:ok, state}
| {:ok, state, :hibernate}
| {:error, reason :: term}
when state: term
| {:error, reason :: any}
when state: any
@callback handle_event(event :: term, state :: term) ::
{:ok, new_state}
+7 -7
View File
@@ -415,7 +415,7 @@ defmodule GenServer do
Let's see how we could use those functions for debugging the stack server
we defined earlier.
iex> {:ok, pid} = Stack.start_link("")
iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
@@ -523,8 +523,8 @@ defmodule GenServer do
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
| :ignore
| {:stop, reason :: term}
when state: term
| {:stop, reason :: any}
when state: any
@doc """
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
@@ -845,7 +845,7 @@ defmodule GenServer do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenServer
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -945,7 +945,7 @@ defmodule GenServer do
end
defmacro __before_compile__(env) do
if not Module.defines?(env.module, {:init, 1}) do
unless Module.defines?(env.module, {:init, 1}) do
message = """
function init/1 required by behaviour GenServer is not implemented \
(in module #{inspect(env.module)}).
@@ -1018,7 +1018,7 @@ defmodule GenServer do
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
"""
@spec start_link(module, term, options) :: on_start
@spec start_link(module, any, options) :: on_start
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:link, module, init_arg, options)
end
@@ -1028,7 +1028,7 @@ defmodule GenServer do
See `start_link/3` for more information.
"""
@spec start(module, term, options) :: on_start
@spec start(module, any, options) :: on_start
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:nolink, module, init_arg, options)
end
+97 -129
View File
@@ -139,85 +139,6 @@ defprotocol Inspect do
# Handle structs in Any
@fallback_to_any true
@impl true
defmacro __deriving__(module, options) do
info = Macro.struct_info!(module, __CALLER__)
fields = Enum.sort(Enum.map(info, & &1.field) -- [:__exception__, :__struct__])
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == Enum.sort(only) and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
filtered_guard =
quote do
var!(field) in unquote(filtered_fields)
end
field_guard =
if optional == [] do
filtered_guard
else
optional_map =
for field <- optional, into: %{} do
default = Enum.find(info, %{}, &(&1.field == field)) |> Map.get(:default, nil)
{field, default}
end
quote do
unquote(filtered_guard) and
not case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
unquote(field_guard),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
@doc """
Converts `term` into an algebra document.
@@ -234,7 +155,7 @@ defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
@@ -254,7 +175,7 @@ defimpl Inspect, for: BitString do
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color_doc(IO.iodata_to_binary(inspected), :string, opts)
color(IO.iodata_to_binary(inspected), :string, opts)
else
inspect_bitstring(term, opts)
end
@@ -265,12 +186,12 @@ defimpl Inspect, for: BitString do
end
defp inspect_bitstring("", opts) do
color_doc("<<>>", :binary, opts)
color("<<>>", :binary, opts)
end
defp inspect_bitstring(bitstring, opts) do
left = color_doc("<<", :binary, opts)
right = color_doc(">>", :binary, opts)
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
end
@@ -307,7 +228,7 @@ end
defimpl Inspect, for: List do
def inspect([], opts) do
color_doc("[]", :list, opts)
color("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
@@ -338,26 +259,19 @@ defimpl Inspect, for: List do
lists
end
open = color_doc("[", :list, opts)
sep = color_doc(",", :list, opts)
close = color_doc("]", :list, opts)
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :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)
color(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
@@ -367,22 +281,9 @@ 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)
key = color(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@@ -400,9 +301,9 @@ end
defimpl Inspect, for: Tuple do
def inspect(tuple, opts) do
open = color_doc("{", :tuple, opts)
sep = color_doc(",", :tuple, opts)
close = color_doc("}", :tuple, opts)
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
end
@@ -421,7 +322,7 @@ defimpl Inspect, for: Map do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color_doc(" => ", :map, opts)
sep = color(" => ", :map, opts)
&to_assoc(&1, &2, sep)
end
@@ -438,9 +339,9 @@ defimpl Inspect, for: Map do
end
defp map_container_doc(list, name, opts, fun) do
open = color_doc("%" <> name <> "{", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc("}", :map, opts)
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
end
end
@@ -448,7 +349,7 @@ end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color_doc(inspected, :number, opts)
color(inspected, :number, opts)
end
defp base_to_value(base) do
@@ -486,10 +387,10 @@ defimpl Inspect, for: Float do
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
[Integer.to_string(trunc(float)), ?., ?0]
else
Float.to_charlist(float)
:erlang.float_to_list(float, [:short])
end
color_doc(IO.iodata_to_binary(formatted), :number, opts)
color(IO.iodata_to_binary(formatted), :number, opts)
end
end
@@ -512,7 +413,7 @@ defimpl Inspect, for: Regex do
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
color_doc(source, :regex, opts)
color(source, :regex, opts)
end
end
@@ -647,6 +548,73 @@ defimpl Inspect, for: Reference do
end
defimpl Inspect, for: Any do
defmacro __deriving__(module, struct, options) do
fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__])
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == Enum.sort(only) and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
optional? =
if optional == [] do
false
else
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
quote do
case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
var!(field) in unquote(filtered_fields) and not unquote(optional?),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
def inspect(%module{} = struct, opts) do
try do
{module.__struct__(), module.__info__(:struct)}
@@ -668,9 +636,9 @@ defimpl Inspect, for: Any do
end
def inspect(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc(">", :map, opts)
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
fun = fn
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
+73 -108
View File
@@ -17,8 +17,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.
@@ -333,7 +333,6 @@ defmodule Inspect.Algebra do
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
# Elixir + Inspect.Opts conveniences
# These have the _doc suffix.
@doc """
Converts an Elixir term to an algebra document
@@ -439,7 +438,7 @@ defmodule Inspect.Algebra do
"""
@doc since: "1.6.0"
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
t
def container_doc(left, collection, right, inspect_opts, fun, opts \\ [])
when is_doc(left) and is_list(collection) and is_doc(right) and is_function(fun, 2) and
@@ -456,7 +455,7 @@ defmodule Inspect.Algebra do
container_each(collection, inspect_opts.limit, inspect_opts, fun, [], break == :maybe)
flex? = simple? or break == :flex
docs = fold(docs, &join(&1, &2, flex?, separator))
docs = fold_doc(docs, &join(&1, &2, flex?, separator))
case flex? do
true -> group(concat(concat(left, nest(docs, 1)), right))
@@ -526,32 +525,6 @@ defmodule Inspect.Algebra do
container_doc(left, docs, right, inspect, fun, separator: separator)
end
# TODO: Deprecate me on Elixir v1.23
@doc deprecated: "Use color_doc/3 instead"
def color(doc, key, opts) do
color_doc(doc, key, opts)
end
@doc ~S"""
Colors a document if the `color_key` has a color in the options.
"""
@doc since: "1.18.0"
@spec color_doc(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
def color_doc(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, ansi(precolor)), doc_color(empty(), ansi(postcolor)))
else
doc
end
end
defp ansi(color) do
color
|> IO.ANSI.format_fragment(true)
|> IO.iodata_to_binary()
end
# Algebra API
@doc """
@@ -646,16 +619,21 @@ defmodule Inspect.Algebra do
"""
@spec concat([t]) :: t
def concat(docs) when is_list(docs) do
fold(docs, &concat(&1, &2))
fold_doc(docs, &concat(&1, &2))
end
@doc ~S"""
Colors a document with the given color (preceding the document itself).
Colors a document if the `color_key` has a color in the options.
"""
@doc since: "1.18.0"
@spec color(t, binary) :: t
def color(doc, color) when is_doc(doc) and is_binary(color) do
doc_color(doc, color)
@doc since: "1.4.0"
@spec color(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
else
doc
end
end
@doc ~S"""
@@ -955,10 +933,6 @@ defmodule Inspect.Algebra do
@spec line(t, t) :: t
def line(doc1, doc2), do: concat(doc1, concat(line(), doc2))
# TODO: Deprecate me on Elixir v1.23
@doc deprecated: "Use fold/2 instead"
def fold_doc(docs, folder_fun), do: fold(docs, folder_fun)
@doc ~S"""
Folds a list of documents into a document using the given folder function.
@@ -970,22 +944,21 @@ defmodule Inspect.Algebra do
iex> docs = ["A", "B", "C"]
iex> docs =
...> Inspect.Algebra.fold(docs, fn doc, acc ->
...> Inspect.Algebra.fold_doc(docs, fn doc, acc ->
...> Inspect.Algebra.concat([doc, "!", acc])
...> end)
iex> Inspect.Algebra.format(docs, 80)
["A", "!", "B", "!", "C"]
"""
@doc since: "1.18.0"
@spec fold([t], (t, t -> t)) :: t
def fold(docs, folder_fun)
@spec fold_doc([t], (t, t -> t)) :: t
def fold_doc(docs, folder_fun)
def fold([], _folder_fun), do: empty()
def fold([doc], _folder_fun), do: doc
def fold_doc([], _folder_fun), do: empty()
def fold_doc([doc], _folder_fun), do: doc
def fold([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold(docs, folder_fun))
def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold_doc(docs, folder_fun))
@doc ~S"""
Formats a given document for a given width.
@@ -1015,17 +988,20 @@ defmodule Inspect.Algebra do
#
# * flat - represents a document with breaks as flats (a break may fit, as it may break)
# * break - represents a document with breaks as breaks (a break always fits, since it breaks)
#
# The following modes are exclusive to fitting:
#
# * flat_no_break - represents a document with breaks as flat not allowed to enter in break mode
# * break_no_flat - represents a document with breaks as breaks not allowed to enter in flat mode
#
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
@spec fits(
@spec fits?(
width :: non_neg_integer() | :infinity,
column :: non_neg_integer(),
break? :: boolean(),
entries
) :: :fit | :no_fit | :break_next
) :: boolean()
when entries:
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
@@ -1034,68 +1010,63 @@ defmodule Inspect.Algebra do
#
# In case we have groups and the group fits, we need to consider the group
# parent without the child breaks, hence {:tail, b?, t} below.
defp fits(w, k, b?, _) when k > w and b?, do: :no_fit
defp fits(_, _, _, []), do: :fit
defp fits(w, k, _, {:tail, b?, t}), do: fits(w, k, b?, t)
defp fits?(w, k, b?, _) when k > w and b?, do: false
defp fits?(_, _, _, []), do: true
defp fits?(w, k, _, {:tail, b?, t}), do: fits?(w, k, b?, t)
## Flat no break
defp fits(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits?(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits?(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
## Breaks no flat
defp fits(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits?(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits?(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits(w, k, b?, [{i, :break_no_flat, x} | t])
when x == :doc_line or (is_tuple(x) and elem(x, 0) == :doc_break) do
case fits(w, k, b?, [{i, :flat, x} | t]) do
:no_fit -> :break_next
fits -> fits
end
end
defp fits?(_, _, _, [{_, :break_no_flat, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break_no_flat, :doc_line} | _]), do: true
## Breaks
defp fits(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: :fit
defp fits(_, _, _, [{_, :break, :doc_line} | _]), do: :fit
defp fits?(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break, :doc_line} | _]), do: true
defp fits(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
do: fits(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
defp fits?(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
## Catch all
defp fits(w, _, _, [{i, _, :doc_line} | t]), do: fits(w, i, false, t)
defp fits(w, k, b?, [{_, _, :doc_nil} | t]), do: fits(w, k, b?, t)
defp fits(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits(w, i, b?, t)
defp fits(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
defp fits(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits(w, k + l, b?, t)
defp fits(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits(w, k + byte_size(s), b?, t)
defp fits(_, _, _, [{_, _, doc_force(_)} | _]), do: :no_fit
defp fits(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits(w, k + byte_size(s), true, t)
defp fits(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
defp fits?(w, _, _, [{i, _, :doc_line} | t]), do: fits?(w, i, false, t)
defp fits?(w, k, b?, [{_, _, :doc_nil} | t]), do: fits?(w, k, b?, t)
defp fits?(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, b?, t)
defp fits?(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
defp fits?(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, b?, t)
defp fits?(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), b?, t)
defp fits?(_, _, _, [{_, _, doc_force(_)} | _]), do: false
defp fits?(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), true, t)
defp fits?(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
defp fits(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
do: fits(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
defp fits?(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
do: fits?(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
defp fits(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
do: fits(w, k, b?, [{i, m, x}, {i, m, y} | t])
defp fits?(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
do: fits?(w, k, b?, [{i, m, x}, {i, m, y} | t])
defp fits(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits(w, k, b?, [{i, m, x} | {:tail, b?, t}])
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
defp fits(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
do: fits(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
defp fits?(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
do: fits?(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
defp fits(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
do: fits(w, k, b?, [{i, m, x} | t])
defp fits?(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
do: fits?(w, k, b?, [{i, m, x} | t])
@spec format(
width :: non_neg_integer() | :infinity,
@@ -1106,14 +1077,10 @@ defmodule Inspect.Algebra do
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x}, {i, m, y} | t])
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [c | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)]
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)]
defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, k, [{i, :flat_no_break, doc_fits(x, :enabled)} | t]),
do: format(w, k, [{i, :break_no_flat, x} | t])
defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i)
@@ -1121,7 +1088,7 @@ defmodule Inspect.Algebra do
defp format(w, k, [{i, m, doc_break(s, :flex)} | t]) do
k = k + byte_size(s)
if w == :infinity or m == :flat or fits(w, k, true, t) != :no_fit do
if w == :infinity or m == :flat or fits?(w, k, true, t) do
[s | format(w, k, t)]
else
[indent(i) | format(w, i, t)]
@@ -1151,17 +1118,11 @@ defmodule Inspect.Algebra do
format(w, k, [{i, :break, x} | t])
end
defp format(w, k, [{i, :break_no_flat, doc_group(x, _)} | t]) do
format(w, k, [{i, :break, x} | t])
end
defp format(w, k, [{i, _, doc_group(x, _)} | t]) do
fits = if w == :infinity, do: :fit, else: fits(w, k, false, [{i, :flat, x}])
case fits do
:fit -> format(w, k, [{i, :flat, x} | t])
:no_fit -> format(w, k, [{i, :break, x} | t])
:break_next -> format(w, k, [{i, :flat_no_break, x} | t])
if w == :infinity or fits?(w, k, false, [{i, :flat, x}]) do
format(w, k, [{i, :flat, x} | t])
else
format(w, k, [{i, :break, x} | t])
end
end
@@ -1190,6 +1151,10 @@ defmodule Inspect.Algebra do
defp apply_nesting(_, _, :reset), do: 0
defp apply_nesting(i, _, j), do: i + j
defp ansi(color) do
IO.ANSI.format_fragment(color, true)
end
defp indent(0), do: @newline
defp indent(i), do: @newline <> :binary.copy(" ", i)
end
+9 -12
View File
@@ -189,16 +189,13 @@ defmodule Integer do
@spec digits(integer, pos_integer) :: [integer, ...]
def digits(integer, base \\ 10)
when is_integer(integer) and is_integer(base) and base >= 2 do
case integer do
0 -> [0]
_integer -> digits(integer, base, [])
end
do_digits(integer, base, [])
end
defp digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
defp digits(integer, base, acc),
do: digits(div(integer, base), base, [rem(integer, base) | acc])
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
@doc """
Returns the integer represented by the ordered `digits`.
@@ -220,16 +217,16 @@ defmodule Integer do
"""
@spec undigits([integer], pos_integer) :: integer
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
undigits(digits, base, 0)
do_undigits(digits, base, 0)
end
defp undigits([], _base, acc), do: acc
defp do_undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
defp do_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)
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
@doc """
Parses a text representation of an integer.
+8 -22
View File
@@ -12,8 +12,6 @@ defmodule IO do
(as shown in typespecs). For more information on chardata, see the
"IO data" section below.
The functions of this module use UNIX-style naming where possible.
## IO devices
An IO device may be an atom or a PID. In case it is an atom,
@@ -133,11 +131,9 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument:
* 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.
iterated by that number of bytes.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRFL newlines ("\r\n") are automatically normalized to "\n".
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
@@ -188,11 +184,9 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument:
* 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.
iterated by that number of bytes.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRFL newlines ("\r\n") are automatically normalized to "\n".
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
@@ -296,8 +290,6 @@ defmodule IO do
By default, the `device` is the standard output. It returns `:ok`
if it succeeds.
Trivia: `puts` is shorthand for `put string`.
## Examples
IO.puts("Hello World!")
@@ -573,8 +565,6 @@ defmodule IO do
for instance, `{:error, :estale}` if reading from an
NFS volume
Trivia: `gets` is shorthand for `get string`.
## Examples
To display "What is your name?" as a prompt and await user input:
@@ -606,9 +596,8 @@ defmodule IO do
`Collectable`, allowing it to be used for both read
and write.
The `device` is iterated by the given number of characters
or line by line if `:line` is given. In case `:line` is given,
"\r\n" is automatically normalized to "\n".
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
This reads from the IO as UTF-8. Check out
`IO.binstream/2` to handle the IO as a raw binary.
@@ -661,17 +650,14 @@ defmodule IO do
`Collectable`, allowing it to be used for both read
and write.
The `device` is iterated by the given number of bytes or line
by line if `:line` is given. In case `:line` is given, "\r\n"
is automatically normalized to "\n". Passing the number of bytes
should be the preferred mode for reading non-textual inputs.
The `device` is iterated by the given number of bytes or line by line if
`:line` is given. This reads from the IO device as a raw binary.
Note that an IO stream has side effects and every time
you go over the stream you may get different results.
This reads from the IO device as a raw binary. Therefore,
do not use this function on IO devices in Unicode mode as
it will return the wrong result.
Finally, do not use this function on IO devices in Unicode
mode as it will return the wrong result.
`binstream/0` has been introduced in Elixir v1.12.0,
while `binstream/2` has been available since v1.0.0.
+1 -1
View File
@@ -350,7 +350,7 @@ defmodule IO.ANSI.Docs do
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
if !no_wrap, do: newline_after_block(options)
unless no_wrap, do: newline_after_block(options)
end
defp format_text(text, options) do
-430
View File
@@ -1,430 +0,0 @@
defprotocol JSON.Encoder do
@moduledoc """
A protocol for custom JSON encoding of data structures.
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: [....]}
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}
>
> The `:except` approach should be used carefully to avoid
> accidentally leaking private information when new fields are added.
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)
"""
@undefined_impl_description """
the protocol must be explicitly implemented.
If you have a struct, you can derive the implementation specifying \
which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields, although this should be \
used carefully to avoid accidentally leaking private information \
when new fields are added:
@derive JSON.Encoder
defstruct ...
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)\
"""
@impl true
defmacro __deriving__(module, opts) do
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
fields = fields_to_encode(fields, opts)
vars = Macro.generate_arguments(length(fields), __MODULE__)
kv = Enum.zip(fields, vars)
{io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end)
io = if io == [], do: "{}", else: io ++ [?}]
quote do
defimpl JSON.Encoder, for: unquote(module) do
def encode(%{unquote_splicing(kv)}, encoder) do
unquote(io)
end
end
end
end
defp fields_to_encode(fields, opts) do
cond do
only = Keyword.get(opts, :only) ->
case only -- fields do
[] ->
only
error_keys ->
raise ArgumentError,
"unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end
except = Keyword.get(opts, :except) ->
case except -- fields do
[] ->
fields -- [:__struct__ | except]
error_keys ->
raise ArgumentError,
"unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end
true ->
fields -- [:__struct__]
end
end
@doc """
A function invoked to encode the given term to `t:iodata/0`.
"""
def encode(term, encoder)
end
defimpl JSON.Encoder, for: Atom do
def encode(value, encoder) do
case value do
nil -> "null"
true -> "true"
false -> "false"
_ -> encoder.(Atom.to_string(value), encoder)
end
end
end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:elixir_json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:elixir_json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:elixir_json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:elixir_json.encode_float(value)
end
end
defimpl JSON.Encoder, for: Map do
def encode(value, encoder) do
case :maps.next(:maps.iterator(value)) do
:none ->
"{}"
{key, value, iterator} ->
[?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
defp next(iterator, encoder) do
case :maps.next(iterator) do
:none ->
"}"
{key, value, iterator} ->
[?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
# Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol.
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)
end
defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do
def encode(value, _encoder) do
[?", @for.to_iso8601(value), ?"]
end
end
defmodule JSON.DecodeError do
@moduledoc """
The exception raised by `JSON.decode!/1`.
"""
defexception [:message, :offset, :data]
end
defmodule JSON do
@moduledoc ~S"""
JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
standards.
## Encoding
Elixir built-in data structures are encoded to JSON as follows:
| **Elixir** | **JSON** |
|----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures.
## Decoding
Elixir built-in data structures are decoded from JSON as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
| Number | `integer() \| float()` |
| Boolean | `true \| false` |
| Null | `nil` |
| String | `binary()` |
| Object | `%{binary() => _}` |
"""
@moduledoc since: "1.18.0"
@type encoder :: (term(), encoder() -> iodata())
@type decode_error_reason ::
{:unexpected_end, non_neg_integer()}
| {:invalid_byte, non_neg_integer(), byte()}
| {:unexpected_sequence, non_neg_integer(), binary()}
@doc ~S"""
Decodes the given JSON.
Returns `{:ok, decoded}` or `{:error, reason}`.
## Examples
iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]")
{:ok, [nil, 123, "string", %{"key" => "value"}]}
## Error reasons
The error tuple will have one of the following reasons.
* `{:unexpected_end, offset}` if `binary` contains incomplete JSON value
* `{:invalid_byte, offset, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte
* `{:unexpected_sequence, offset, bytes}` if `binary` contains invalid UTF-8 escape
"""
@spec decode(binary()) :: {:ok, term()} | {:error, decode_error_reason()}
def decode(binary) when is_binary(binary) do
with {decoded, :ok, rest} <- decode(binary, :ok, []) do
if rest == "" do
{:ok, decoded}
else
{:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}}
end
end
end
@doc ~S"""
Decodes the given JSON with the given decoders.
Returns `{decoded, acc, rest}` or `{:error, reason}`.
See `decode/1` for the error reasons.
## Decoders
All decoders are optional. If not provided, they will fall back to
implementations used by the `decode/1` function:
* for `array_start`: `fn _ -> [] end`
* for `array_push`: `fn elem, acc -> [elem | acc] end`
* for `array_finish`: `fn acc, old_acc -> {Enum.reverse(acc), old_acc} end`
* for `object_start`: `fn _ -> [] end`
* for `object_push`: `fn key, value, acc -> [{key, value} | acc] end`
* for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end`
* for `float`: `&String.to_float/1`
* for `integer`: `&String.to_integer/1`
* for `string`: `&Function.identity/1`
* for `null`: the atom `nil`
For streaming decoding, see Erlang's `:json` module.
"""
@spec decode(binary(), term(), keyword()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:elixir_json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
:error, {:invalid_byte, byte} ->
{:error, {:invalid_byte, offset(__STACKTRACE__), byte}}
:error, {:unexpected_sequence, bytes} ->
{:error, {:unexpected_sequence, offset(__STACKTRACE__), bytes}}
end
end
defp offset(stacktrace) do
with [{_, _, _, opts} | _] <- stacktrace,
%{cause: %{position: position}} <- opts[:error_info] do
position
else
_ -> 0
end
end
@doc ~S"""
Decodes the given JSON but raises an exception in case of errors.
Returns the decoded content. See `decode/1` for possible errors.
## Examples
iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]")
[nil, 123, "string", %{"key" => "value"}]
"""
@spec decode!(binary()) :: term()
def decode!(binary) when is_binary(binary) do
case decode(binary) do
{:ok, decoded} ->
decoded
{:error, {:unexpected_end, offset}} ->
raise JSON.DecodeError,
message: "unexpected end of JSON binary at position (byte offset) #{offset}",
data: binary,
offset: offset
{:error, {:invalid_byte, offset, byte}} ->
raise JSON.DecodeError,
message: "invalid byte #{byte} at position (byte offset) #{offset}",
data: binary,
offset: offset
{:error, {:unexpected_sequence, offset, bytes}} ->
raise JSON.DecodeError,
message: "unexpected sequence #{inspect(bytes)} at position (byte offset) #{offset}",
data: binary,
offset: offset
end
end
@doc ~S"""
Encodes the given term to JSON as a binary.
The second argument is a function that is recursively
invoked to encode a term.
> #### IO and performance {: .tip}
>
> If you need to encode data to be sent over the network
> or written to the filesystem, consider using the more
> efficient `encode_to_iodata!/2`.
## Examples
iex> JSON.encode!([123, "string", %{key: "value"}])
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode!(term(), encoder()) :: binary()
def encode!(term, encoder \\ &protocol_encode/2) do
IO.iodata_to_binary(encoder.(term, encoder))
end
@doc ~S"""
Encodes the given term to JSON as an iodata.
This is the most efficient format if the JSON is going to be
used for IO purposes.
The second argument is a function that is recursively
invoked to encode a term.
## Examples
iex> data = JSON.encode_to_iodata!([123, "string", %{key: "value"}])
iex> IO.iodata_to_binary(data)
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode_to_iodata!(term(), encoder()) :: iodata()
def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
encoder.(term, encoder)
end
@doc """
This is the default encode implementation passed to `encode!/1`.
This function is most typically passed as second argument to
`encode!/2` and `encode_to_iodata!/2`. The default implementation
is an optimized dispatch to the `JSON.Encoder` protocol.
"""
@spec protocol_encode(term(), encoder()) :: iodata()
def protocol_encode(value, encoder) when is_atom(value) do
case value do
nil -> "null"
true -> "true"
false -> "false"
_ -> encoder.(Atom.to_string(value), encoder)
end
end
def protocol_encode(value, _encoder) when is_binary(value),
do: :elixir_json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value),
do: :elixir_json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value),
do: :elixir_json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value),
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)
def protocol_encode(value, encoder),
do: JSON.Encoder.encode(value, encoder)
end
+121 -147
View File
@@ -238,7 +238,7 @@ defmodule Kernel do
When comparing two numbers of different types (a number being either
an integer or a float), a conversion to the type with greater precision
will always occur, unless the comparison operator used is either `===/2`
or `!==/2`. A float will be considered more precise than an integer, unless
or `!==`. A float will be considered more precise than an integer, unless
the float is greater/less than +/-9007199254740992.0 respectively,
at which point all the significant figures of the float are to the left
of the decimal point. This behavior exists so that the comparison of large
@@ -247,7 +247,9 @@ defmodule Kernel do
The collection types are compared using the following rules:
* Tuples are compared by size, then element by element.
* Maps are compared by size, then by key-value pairs.
* Maps are compared by size, then by keys in ascending term order,
then by values in key order. In the specific case of maps' key
ordering, integers are always considered to be less than floats.
* Lists are compared element by element.
* Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit.
* Atoms are compared using their string value, codepoint by codepoint.
@@ -300,7 +302,7 @@ defmodule Kernel do
@compile {:inline, bootstrapped?: 1}
case :code.ensure_loaded(Kernel) do
{:module, _} ->
defp bootstrapped?(module), do: is_atom(module)
defp bootstrapped?(_), do: true
{:error, _} ->
defp bootstrapped?(module), do: :code.ensure_loaded(module) == {:module, module}
@@ -628,7 +630,7 @@ defmodule Kernel do
"""
@doc guard: true
@spec hd(nonempty_maybe_improper_list(elem, term)) :: elem when elem: term
@spec hd(nonempty_maybe_improper_list(elem, any)) :: elem when elem: term
def hd(list) do
:erlang.hd(list)
end
@@ -1596,7 +1598,7 @@ defmodule Kernel do
This function is optimized so the complexity of `a -- b` is proportional
to `length(a) * log(length(b))`. See also the [Erlang efficiency
guide](https://www.erlang.org/doc/system/efficiency_guide.html).
guide](https://www.erlang.org/doc/efficiency_guide/retired_myths.html).
Inlined by the compiler.
@@ -1903,8 +1905,8 @@ defmodule Kernel do
## Implemented in Elixir
defp annotate_case(extra, {:case, meta, args}) do
{:case, extra ++ meta, args}
defp optimize_boolean({:case, meta, args}) do
{:case, [{:optimize_boolean, true} | meta], args}
end
@doc """
@@ -1971,8 +1973,7 @@ defmodule Kernel do
end
defp build_boolean_check(operator, check, true_clause, false_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
optimize_boolean(
quote do
case unquote(check) do
false -> unquote(false_clause)
@@ -2005,8 +2006,7 @@ defmodule Kernel do
defmacro !{:!, _, [value]} do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
optimize_boolean(
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
@@ -2019,8 +2019,7 @@ defmodule Kernel do
defmacro !value do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
optimize_boolean(
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
@@ -2218,22 +2217,28 @@ defmodule Kernel do
case Macro.expand(message, __CALLER__) do
message when is_binary(message) ->
quote do
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
:erlang.error(
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
)
end
{:<<>>, _, _} = message ->
quote do
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
:erlang.error(
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
)
end
alias when is_atom(alias) ->
quote do
:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace))
:erlang.error(:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace)))
end
message ->
quote do
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
:erlang.error(
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
)
end
end
end
@@ -2447,6 +2452,8 @@ defmodule Kernel do
* when updating a struct, as in `struct!(%SomeStruct{}, key: :value)`,
it is equivalent to `%SomeStruct{struct | key: :value}` and therefore this
function will check if every given key-value belongs to the struct.
However, updating structs does not enforce keys, as keys are enforced
only when building;
"""
@spec struct!(module | struct, Enumerable.t()) :: struct
@@ -2966,7 +2973,7 @@ defmodule Kernel do
(term | nil -> {current_value, new_value} | :pop)
) :: {current_value, new_structure :: structure}
when structure: Access.t(),
keys: nonempty_list(term),
keys: nonempty_list(any),
current_value: Access.value(),
new_value: Access.value()
def get_and_update_in(data, keys, fun)
@@ -3306,7 +3313,7 @@ defmodule Kernel do
end
defp nest_pop_in(:map, h, [{:access, key}]) do
quote generated: true do
quote do
case unquote(h) do
nil -> {nil, nil}
h -> Access.pop(h, unquote(key))
@@ -3327,7 +3334,7 @@ defmodule Kernel do
end
defp nest_pop_in(_, h, [{:access, key}]) do
quote generated: true do
quote do
case unquote(h) do
nil -> :pop
h -> Access.pop(h, unquote(key))
@@ -3417,7 +3424,7 @@ defmodule Kernel do
## Examples
iex> is_nil(1 + 2)
iex> is_nil(1)
false
iex> is_nil(nil)
@@ -3642,7 +3649,7 @@ defmodule Kernel do
:ok
true ->
pos = :elixir_module.cache_env(__CALLER__)
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
@@ -3716,25 +3723,26 @@ defmodule Kernel do
case function? do
true ->
case Module.__get_attribute__(env.module, name, line, false) do
{_, doc} when doc_attr? ->
do_at_escape(name, doc)
value =
case Module.__get_attribute__(env.module, name, line, false) do
{_, doc} when doc_attr? -> doc
other -> other
end
%{__struct__: Regex, source: source, opts: opts} = regex ->
case :erlang.system_info(:otp_release) < [?2, ?8] do
true -> do_at_escape(name, regex)
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
end
value ->
do_at_escape(name, value)
try do
:elixir_quote.escape(value, :none, false)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
"cannot inject attribute @#{name} into function/macro because " <>
Exception.message(ex)
end
false when doc_attr? ->
quote do
case Module.__get_attribute__(__MODULE__, unquote(name), unquote(line), false) do
{_, doc} -> doc
value -> value
other -> other
end
end
@@ -3769,17 +3777,6 @@ defmodule Kernel do
raise ArgumentError, "expected 0 or 1 argument for @#{name}, got: #{length(args)}"
end
defp do_at_escape(name, value) do
try do
:elixir_quote.escape(value, :none, false)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
"cannot inject attribute @#{name} into function/macro because " <>
Exception.message(ex)
end
end
# Those are always compile-time dependencies, so we can skip the trace.
defp collect_traces(:before_compile, arg, _env), do: {arg, []}
defp collect_traces(:after_compile, arg, _env), do: {arg, []}
@@ -3919,8 +3916,7 @@ defmodule Kernel do
end
defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
optimize_boolean(
quote do
case unquote(condition) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
@@ -3960,8 +3956,6 @@ defmodule Kernel do
"Math still works"
"""
# TODO: Deprecate this on Elixir v1.22.
@doc deprecated: "Use if/2 instead"
defmacro unless(condition, clauses) do
build_unless(condition, clauses)
end
@@ -4076,7 +4070,7 @@ defmodule Kernel do
end
defp stepless_range(nil, first, last, _caller) do
quote(do: Function.identity(Elixir.Range.new(unquote(first), unquote(last))))
quote(do: Elixir.Range.new(unquote(first), unquote(last)))
end
defp stepless_range(:guard, first, last, caller) do
@@ -4238,18 +4232,15 @@ defmodule Kernel do
defmacro left && right do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
annotate_case(
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x
_ ->
unquote(right)
end
_ ->
unquote(right)
end
)
end
end
@doc """
@@ -4281,18 +4272,15 @@ defmodule Kernel do
defmacro left || right do
assert_no_match_or_guard_scope(__CALLER__.context, "||")
annotate_case(
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)
x ->
x
end
x ->
x
end
)
end
end
@doc """
@@ -4594,9 +4582,6 @@ defmodule Kernel do
defp in_var(true, {atom, _, context} = var, fun) when is_atom(atom) and is_atom(context),
do: fun.(var)
defp in_var(true, var, fun) when is_atom(var) or is_binary(var) or is_number(var),
do: fun.(var)
defp in_var(true, ast, fun) do
quote do
var = unquote(ast)
@@ -5023,14 +5008,12 @@ defmodule Kernel do
assert_no_match_or_guard_scope(env.context, "defmodule/2")
expanded = expand_module_alias(alias, env)
module_meta = module_meta(alias)
{expanded, with_alias} =
case is_atom(expanded) do
true ->
{full, old, opts} = alias_defmodule(alias, expanded, env)
# Expand the module considering the current environment/nesting
meta = [defined: full] ++ module_meta
meta = [defined: full] ++ alias_meta(alias)
{full, {:require, meta, [old, opts]}}
false ->
@@ -5062,7 +5045,6 @@ defmodule Kernel do
unquote(with_alias)
:elixir_module.compile(
unquote(module_meta),
unquote(expanded),
unquote(escaped),
unquote(module_vars),
@@ -5072,8 +5054,8 @@ defmodule Kernel do
end
end
defp module_meta({_, meta, _}), do: meta
defp module_meta(_), do: []
defp alias_meta({:__aliases__, meta, _}), do: meta
defp alias_meta(_), do: []
# We don't want to trace :alias_reference since we are defining the alias
defp expand_module_alias({:__aliases__, meta, list} = alias, env) do
@@ -5332,7 +5314,7 @@ defmodule Kernel do
key
end
pos = :elixir_module.cache_env(env)
pos = :elixir_locals.cache_env(env)
quote do
:elixir_def.store_definition(unquote(kind), unquote(store), unquote(pos))
@@ -5344,10 +5326,13 @@ defmodule Kernel do
A struct is a tagged map that allows developers to provide
default values for keys, tags to be used in polymorphic
dispatches and compile time assertions.
dispatches and compile time assertions. For more information
about structs, please check `%/2`.
It is only possible to define a struct per module, as the
struct is tied to the module itself.
struct is tied to the module itself. Calling `defstruct/1`
also defines a `__struct__/0` function that returns the
struct itself.
## Examples
@@ -5381,13 +5366,6 @@ defmodule Kernel do
defstruct [:title, :content, :author]
end
Once a struct is defined, it is possible to create them as follows:
%Post{title: "Hello world!"}
For more information on creating, updating, and pattern matching on
structs, please check `%/2`.
## Deriving
Although structs are maps, by default structs do not implement
@@ -5417,12 +5395,12 @@ defmodule Kernel do
defstruct name: nil, age: nil
end
For each protocol in `@derive`, Elixir will verify if the protocol
has implemented the `c:Protocol.__deriving__/2` callback. If so,
the callback will be invoked and it should define the implementation
module. Otherwise an implementation that simply points to the `Any`
implementation is automatically derived. For more information, see
`Protocol.derive/3`.
For each protocol in `@derive`, Elixir will assert the protocol has
been implemented for `Any`. If the `Any` implementation defines a
`__deriving__/3` callback, the callback will be invoked and it should define
the implementation module. Otherwise an implementation that simply points to
the `Any` implementation is automatically derived. For more information on
the `__deriving__/3` callback, see `Protocol.derive/3`.
## Enforcing keys
@@ -5474,7 +5452,7 @@ defmodule Kernel do
"""
defmacro defstruct(fields) do
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
{struct, derive, escaped_struct, kv, body} =
{struct, derive, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
case derive do
@@ -5482,7 +5460,7 @@ defmodule Kernel do
_ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
end
def __struct__(), do: unquote(escaped_struct)
def __struct__(), do: @__struct__
def __struct__(unquote(kv)), do: unquote(body)
Kernel.Utils.announce_struct(__MODULE__)
@@ -5569,8 +5547,27 @@ defmodule Kernel do
# it is the bootstrapped version or not.
Elixir.Kernel.@(impl(true))
# TODO: Change the implementation on v2.0 to simply call Kernel.struct!/2
def exception(args) when Kernel.is_list(args) do
struct!(__MODULE__, args)
struct = __struct__()
{valid, invalid} = Enum.split_with(args, fn {k, _} -> Map.has_key?(struct, k) end)
case invalid do
[] ->
:ok
_ ->
IO.warn(
"the following fields are unknown when raising " <>
"#{Kernel.inspect(__MODULE__)}: #{Kernel.inspect(invalid)}. " <>
"Please make sure to only give known fields when raising " <>
"or redefine #{Kernel.inspect(__MODULE__)}.exception/1 to " <>
"discard unknown fields. Future Elixir versions will raise on " <>
"unknown fields given to raise/2"
)
end
Kernel.struct!(struct, valid)
end
defoverridable exception: 1
@@ -5695,15 +5692,16 @@ defmodule Kernel do
end
@doc """
Defines a macro suitable for use in guard expressions.
Generates a macro suitable for use in guard expressions.
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.
It raises at compile time if the definition uses expressions that aren't
allowed in 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)
around boolean-returning guards.
Note the convention in Elixir is to prefix all guards that return a boolean
with the `is_` prefix, such as `is_list/1`. If, however, the function/macro
returns a boolean and is not allowed in guards, it should have no prefix and
end with a question mark, such as `Keyword.keyword?/1`.
## Example
@@ -5740,17 +5738,12 @@ defmodule Kernel do
end
@doc """
Defines a private macro suitable for use in guard expressions.
Generates a private macro suitable for use in guard expressions.
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 private macro that can be used
It raises at compile time if the definition uses expressions that aren't
allowed in guards, and otherwise creates a private macro that can be used
both inside or outside guards in the current module.
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
around boolean-returning guards.
Similar to `defmacrop/2`, `defguardp/1` must be defined before its use
in the current module.
"""
@@ -5768,11 +5761,8 @@ defmodule Kernel do
"only a single when clause is allowed"
{call, impls} ->
case decompose_args(call) do
:error ->
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
args ->
case Macro.decompose_call(call) do
{_name, args} ->
validate_variable_only_args!(call, args)
macro_definition =
@@ -5794,20 +5784,13 @@ defmodule Kernel do
Elixir.Kernel.@(doc(guard: true))
unquote(macro_definition)
end
_invalid_definition ->
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
end
end
end
defp decompose_args({name, _, args}) when is_atom(name) and is_atom(args), do: []
defp decompose_args({name, _, args}) when is_atom(name) and is_list(args), do: args
defp decompose_args({{:unquote, _, _}, _, args}) when is_atom(args), do: []
defp decompose_args({{:unquote, _, _}, _, args}) when is_list(args), do: args
defp decompose_args(_), do: :error
defp validate_variable_only_args!(call, args) do
Enum.each(args, fn
{ref, _meta, context} when is_atom(ref) and is_atom(context) ->
@@ -6447,38 +6430,29 @@ defmodule Kernel do
"""
defmacro sigil_r(term, modifiers)
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1)
compile_regex(binary, options)
defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do
binary = :elixir_interpolation.unescape_string(string, &regex_unescape_map/1)
regex = Regex.compile!(binary, :binary.list_to_bin(options))
Macro.escape(regex)
end
defmacro sigil_r({:<<>>, meta, pieces}, options) do
tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
compile_regex(tuple, options)
binary = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options))))
end
defp regex_unescape_map(:newline), do: true
defp regex_unescape_map(_), do: false
@doc false
defmacro sigil_R({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do
IO.warn(
"~R/.../ is deprecated, use ~r/.../ instead",
Macro.Env.stacktrace(__CALLER__)
)
compile_regex(binary, options)
end
defp compile_regex(binary_or_tuple, options) do
# TODO: Remove this when we require Erlang/OTP 28+
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
true ->
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
false ->
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
end
regex = Regex.compile!(string, :binary.list_to_bin(options))
Macro.escape(regex)
end
@doc ~S"""
@@ -6716,7 +6690,7 @@ defmodule Kernel do
defp to_calendar_struct(type, fields) do
quote do
%unquote(type){unquote_splicing(fields)}
%{unquote_splicing([__struct__: type] ++ fields)}
end
end
+11 -17
View File
@@ -9,7 +9,6 @@ defmodule Kernel.CLI do
compile: [],
no_halt: false,
compiler_options: [],
warnings_as_errors: false,
errors: [],
verbose_compile: false,
profile: nil,
@@ -196,7 +195,7 @@ defmodule Kernel.CLI do
end
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
@@ -295,16 +294,6 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]})
end
defp parse_argv([~c"--color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, true)
parse_argv(t, config)
end
defp parse_argv([~c"--no-color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, false)
parse_argv(t, config)
end
## Compiler
defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do
@@ -326,7 +315,8 @@ defmodule Kernel.CLI do
end
defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | warnings_as_errors: true})
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
parse_argv(t, %{config | compiler_options: compiler_options})
end
defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do
@@ -509,11 +499,15 @@ defmodule Kernel.CLI do
]
end
output = IO.chardata_to_string(config.output)
profile_opts =
if config.profile do
[profile: config.profile]
else
[]
end
opts =
verbose_opts ++
[profile: config.profile, warnings_as_errors: config.warnings_as_errors]
output = IO.chardata_to_string(config.output)
opts = verbose_opts ++ profile_opts
case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do
{:ok, _, _} -> :ok
+26 -41
View File
@@ -8,7 +8,6 @@ defmodule Kernel.LexicalTracker do
@moduledoc false
@timeout :infinity
@behaviour :gen_server
@warn_key 0
@doc """
Returns all references in this lexical scope.
@@ -17,24 +16,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout)
end
@doc """
Invoked during module expansion to annotate an alias
must be warned if unused.
"""
def warn_alias(pid, meta, alias, module) do
:gen_server.cast(pid, {:warn_alias, alias, meta})
module
end
@doc """
Invoked during module expansion to annotate an import
must be warned if unused.
"""
def warn_import(pid, module) do
:gen_server.cast(pid, {:warn_import, module})
module
end
# Internal API
# Starts the tracker and returns its PID.
@@ -58,6 +39,11 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
end
@doc false
def add_alias(pid, module, meta, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_alias, module, meta, warn})
end
@doc false
def remote_dispatch(pid, module, mode) when is_atom(module) do
:gen_server.cast(pid, {:remote_dispatch, module, mode})
@@ -107,12 +93,16 @@ defmodule Kernel.LexicalTracker do
@doc false
def collect_unused_imports(pid) do
:gen_server.call(pid, :unused_imports, @timeout)
unused(pid, :unused_imports)
end
@doc false
def collect_unused_aliases(pid) do
:gen_server.call(pid, :unused_aliases, @timeout)
unused(pid, :unused_aliases)
end
defp unused(pid, tag) do
:gen_server.call(pid, tag, @timeout)
end
# Callbacks
@@ -133,17 +123,11 @@ defmodule Kernel.LexicalTracker do
@doc false
def handle_call(:unused_aliases, _from, state) do
aliases = for {alias, meta} when is_list(meta) <- state.aliases, do: {alias, meta}
{:reply, Enum.sort(aliases), state}
{:reply, Enum.sort(state.aliases), state}
end
def handle_call(:unused_imports, _from, state) do
imports =
for {module, %{@warn_key => _} = map} <- state.imports do
{module, Map.delete(map, @warn_key)}
end
{:reply, Enum.sort(imports), state}
{:reply, Enum.sort(state.imports), state}
end
def handle_call(:references, _from, state) do
@@ -187,8 +171,8 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | imports: imports, references: references}}
end
def handle_cast({:alias_dispatch, module}, state) do
{:noreply, put_in(state.aliases[module], :used)}
def handle_cast({:alias_dispatch, module}, %{aliases: aliases} = state) do
{:noreply, %{state | aliases: Map.delete(aliases, module)}}
end
def handle_cast({:import_quoted, module, function, arities}, state) do
@@ -226,19 +210,20 @@ defmodule Kernel.LexicalTracker do
end
def handle_cast({:add_import, module, fas, meta, warn}, state) do
keys = if warn, do: [@warn_key, module | fas], else: [module | fas]
{:noreply, put_in(state.imports[module], Map.from_keys(keys, meta))}
end
def handle_cast({:warn_alias, alias, meta}, %{aliases: aliases} = state) do
case aliases do
%{^alias => :used} -> {:noreply, state}
%{} -> {:noreply, %{state | aliases: Map.put(aliases, alias, meta)}}
if warn do
imports = for module_or_fa <- [module | fas], do: {module_or_fa, meta}, into: %{}
{:noreply, put_in(state.imports[module], imports)}
else
{:noreply, state}
end
end
def handle_cast({:warn_import, module}, state) do
{:noreply, put_in(state.imports[module][@warn_key], true)}
def handle_cast({:add_alias, module, meta, warn}, state) do
if warn do
{:noreply, put_in(state.aliases[module], meta)}
else
{:noreply, state}
end
end
@doc false
+50 -49
View File
@@ -31,11 +31,11 @@ defmodule Kernel.ParallelCompiler do
dest = :erlang.get(:elixir_compiler_dest)
{:error_handler, error_handler} = :erlang.process_info(self(), :error_handler)
{_parent, cache} = Module.ParallelChecker.get()
{_parent, checker} = Module.ParallelChecker.get()
task =
Task.async(fn ->
Module.ParallelChecker.put(compiler_pid, cache)
Module.ParallelChecker.put(compiler_pid, checker)
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
@@ -158,10 +158,6 @@ 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
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
@@ -208,10 +204,6 @@ defmodule Kernel.ParallelCompiler do
* `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
"""
@doc since: "1.6.0"
@spec require([Path.t()], keyword()) ::
@@ -247,19 +239,36 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(files, output, options) do
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
schedulers =
Keyword.get_lazy(options, :max_concurrency, fn ->
max(:erlang.system_info(:schedulers_online), 2)
end)
{:ok, cache} = Module.ParallelChecker.start_link(schedulers)
schedulers = max(:erlang.system_info(:schedulers_online), 2)
{:ok, checker} = Module.ParallelChecker.start_link(schedulers)
{status, modules_or_errors, info} =
try do
spawn_workers(schedulers, cache, files, output, options)
outcome = spawn_workers(schedulers, checker, files, output, options)
{outcome, Code.get_compiler_option(:warnings_as_errors)}
else
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true}
when r_warnings != [] or c_warnings != [] ->
message =
"Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
{:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
{{:ok, outcome, info}, _} ->
beam_timestamp = Keyword.get(options, :beam_timestamp)
{:ok, write_module_binaries(outcome, output, beam_timestamp), info}
{{:error, errors, info}, true} ->
%{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info
info = %{info | runtime_warnings: [], compile_warnings: []}
{:error, c_warnings ++ r_warnings ++ errors, info}
{{:error, errors, info}, _} ->
{:error, errors, info}
after
Module.ParallelChecker.stop(cache)
Module.ParallelChecker.stop(checker)
end
# TODO: Require this to be set from Elixir v1.19
@@ -281,7 +290,6 @@ defmodule Kernel.ParallelCompiler do
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
beam_timestamp: Keyword.get(options, :beam_timestamp),
dest: Keyword.get(options, :dest),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
@@ -339,40 +347,38 @@ defmodule Kernel.ParallelCompiler do
## Verification
defp verify_modules(result, compile_warnings, dependent_modules, state) do
modules = write_module_binaries(result, state.output, state.beam_timestamp)
runtime_warnings = maybe_check_modules(result, dependent_modules, state)
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, modules, info}, state}
{{:ok, result, info}, state}
end
defp maybe_check_modules(result, runtime_modules, state) do
%{profile: profile, checker: checker} = state
compiled_modules =
for {{:module, module}, binary} when is_binary(binary) <- result,
do: module
profile(
state,
fn ->
num_modules = length(compiled_modules) + length(runtime_modules)
"group pass check of #{num_modules} modules"
end,
fn -> Module.ParallelChecker.verify(state.checker, runtime_modules) end
)
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(checker, runtime_modules)
end)
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
defp profile_init(nil), do: :none
defp profile(%{profile: :none}, _what, fun), do: fun.()
defp profile(%{profile: {:time, _, _}}, what, fun) do
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
what = if is_binary(what), do: what, else: what.()
IO.puts(:stderr, "[profile] Finished #{what} in #{time}ms")
num_modules = length(compiled_modules) + length(runtime_modules)
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
result
end
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
fun.()
end
## Compiler worker spawning
# We already have n=schedulers currently running, don't spawn new ones
@@ -408,13 +414,13 @@ defmodule Kernel.ParallelCompiler do
end
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, errors, state) do
%{output: output, dest: dest, checker: cache} = state
%{output: output, dest: dest, checker: checker} = state
parent = self()
file = Path.expand(file)
{pid, ref} =
:erlang.spawn_monitor(fn ->
Module.ParallelChecker.put(parent, cache)
Module.ParallelChecker.put(parent, checker)
:erlang.put(:elixir_compiler_info, {parent, self()})
:erlang.put(:elixir_compiler_file, file)
@@ -456,10 +462,7 @@ defmodule Kernel.ParallelCompiler do
end)
[] = files
cycle_return =
profile(state, "cycle resolution", fn -> each_cycle_return(state.each_cycle.()) end)
cycle_return = each_cycle_return(state.each_cycle.())
state = cycle_timing(result, state)
case cycle_return do
@@ -503,15 +506,13 @@ defmodule Kernel.ParallelCompiler do
# Finally, note there is no difference between hard and raise, the
# difference is where the raise is happening, inside the compiler
# or in the caller.
deadlocked =
profile(state, "deadlock resolution", fn ->
waiting_list = Map.to_list(waiting)
waiting_list = Map.to_list(waiting)
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
end)
deadlocked =
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
if deadlocked do
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
+16 -23
View File
@@ -112,24 +112,26 @@ defmodule Kernel.SpecialForms do
%User{age: age} = user
An update operation specific for structs is also available:
%User{user | age: 28}
The advantage of structs is that they validate that the given
keys are part of the defined struct. The example below will fail
because there is no key `:full_name` in the `User` struct:
%User{full_name: "john doe"}
An update operation specific for structs is also available:
The syntax above will guarantee the given keys are valid at
compilation time and it will guarantee at runtime the given
argument is a struct, failing with `BadStructError` otherwise.
%User{user | age: 28}
Once again, the syntax above will guarantee the given keys
are valid at compilation time and it will guarantee at runtime
the given argument is a struct, failing with `BadStructError`
otherwise. The map update syntax can also be used for updating
structs, and it is useful when you want to update any struct,
regardless of their name, as long as they have matching fields:
%{user | age: 28}
Although structs are maps, by default structs do not implement
any of the protocols implemented for maps. Check
`Kernel.defprotocol/2` for more information on how structs
can be used with protocols for polymorphic dispatch. Also
see `Kernel.struct/2` and `Kernel.struct!/2` for examples on
how to create and update structs dynamically.
## Pattern matching on struct names
@@ -1046,7 +1048,7 @@ defmodule Kernel.SpecialForms do
end
end
require ContextHygiene
require Hygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1
@@ -1539,7 +1541,7 @@ defmodule Kernel.SpecialForms do
@doc """
Combine matching clauses.
One of the ways to understand `with` is to show which code
One of the ways to understand with is to show which code
patterns it improves. Imagine you have a map where the fields
`width` and `height` are optional and you want to compute its
area, as `{:ok, area}` or return `:error`. We could implement
@@ -1661,7 +1663,7 @@ defmodule Kernel.SpecialForms do
...> end
{:error, :wrong_data}
The `else` block works like a `case`: it can have multiple clauses,
The `else` block works like a `case` clause: it can have multiple clauses,
and the first match will be used. Variables bound inside `with` (such as
`width` in this example) are not available in the `else` block.
@@ -1786,15 +1788,6 @@ defmodule Kernel.SpecialForms do
&local_function/1
Note that `&local_function/1` creates a local capture, but
`&__MODULE__.local_function/1` or `&imported_function/1` create a remote
capture. For more information, refer to the ["Functions" section in the Erlang Reference Manual](https://www.erlang.org/doc/system/eff_guide_functions.html#function-calls).
Whether a capture is local or remote has implications when using hot code
reloading: local captures dispatch to the version of the module that existed
at the time they were created, while remote captures dispatch to the current
version of the module.
See also `Function.capture/3`.
## Anonymous functions
+39 -50
View File
@@ -118,7 +118,7 @@ defmodule Kernel.Typespec do
case spec_to_signature(expr) do
{name, arity} ->
# store doc only once in case callback has multiple clauses
if not :ets.member(set, {kind, name, arity}) do
unless :ets.member(set, {kind, name, arity}) do
{line, doc} = get_doc_info(set, :doc, line)
store_doc(set, kind, name, arity, line, :doc, doc, %{})
end
@@ -138,7 +138,7 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = signature when signature in @reserved_signatures ->
compile_error(
:elixir_module.get_cached_env(pos),
:elixir_locals.get_cached_env(pos),
"type #{name}/#{arity} is a reserved type and it cannot be defined"
)
@@ -247,7 +247,7 @@ defmodule Kernel.Typespec do
defp collect_defined_type_pairs(type_typespecs) do
fun = fn {_kind, expr, pos}, type_pairs ->
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
%{file: file, line: line} = env = :elixir_locals.get_cached_env(pos)
case type_to_signature(expr) do
{name, arity} = type_pair ->
@@ -290,9 +290,8 @@ defmodule Kernel.Typespec do
:lists.filter(fun, types)
end
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
when is_list(meta) do
caller = :elixir_module.get_cached_env(pos)
defp translate_type({kind, {:"::", _, [{name, _, args}, definition]}, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
state = clean_local_state(state)
args =
@@ -321,7 +320,7 @@ defmodule Kernel.Typespec do
invalid_args = :lists.filter(&(not valid_variable_ast?(&1)), args)
if invalid_args != [] do
unless invalid_args == [] do
invalid_args = :lists.join(", ", :lists.map(&Macro.to_string/1, invalid_args))
message =
@@ -336,7 +335,7 @@ defmodule Kernel.Typespec do
IO.warn(message, caller)
end
{{kind, {name, arity}, meta, type, export}, state}
{{kind, {name, arity}, caller.line, type, export}, state}
end
defp valid_variable_ast?({variable_name, _, context})
@@ -349,17 +348,17 @@ defmodule Kernel.Typespec do
defp underspecified?(_kind, _arity, _spec), do: false
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}, state) do
caller = :elixir_module.get_cached_env(pos)
caller = :elixir_locals.get_cached_env(pos)
translate_spec(kind, spec, guard, caller, state)
end
defp translate_spec({kind, spec, pos}, state) do
caller = :elixir_module.get_cached_env(pos)
caller = :elixir_locals.get_cached_env(pos)
translate_spec(kind, spec, [], caller, state)
end
defp translate_spec(kind, {:"::", _, [{name, meta, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" and is_list(meta) and (is_list(args) or is_atom(args)) do
defp translate_spec(kind, {:"::", meta, [{name, _, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" do
translate_spec(kind, meta, name, args, return, guard, caller, state)
end
@@ -381,7 +380,7 @@ defmodule Kernel.Typespec do
ensure_no_defaults!(args)
state = clean_local_state(state)
if not Keyword.keyword?(guard) do
unless Keyword.keyword?(guard) do
error = "expected keywords as guard in type specification, got: #{Macro.to_string(guard)}"
compile_error(caller, error)
end
@@ -402,7 +401,7 @@ defmodule Kernel.Typespec do
ensure_no_unused_local_vars!(caller, state.local_vars)
arity = length(args)
{{kind, {name, arity}, meta, spec}, state}
{{kind, {name, arity}, caller.line, spec}, state}
end
# TODO: Remove char_list type by v2.0
@@ -493,24 +492,6 @@ defmodule Kernel.Typespec do
state}
end
defp typespec(
{:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]},
_,
_,
state
)
when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do
location = location(meta)
prod_location = location(prod_meta)
{{:type, location, :binary,
[
{:op, prod_location, :*, {:integer, prod_location, size},
{:integer, prod_location, unit}},
{:integer, location, 0}
]}, state}
end
defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
when is_atom(ctx) and is_integer(size) and size >= 0 do
location = location(meta)
@@ -586,23 +567,27 @@ defmodule Kernel.Typespec do
defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
case Macro.expand(name, %{caller | function: {:__info__, 1}}) do
module when is_atom(module) ->
struct_info = Macro.struct_info!(module, caller)
struct =
module
|> Macro.struct!(caller)
|> Map.delete(:__struct__)
|> Map.to_list()
if not Keyword.keyword?(fields) do
unless Keyword.keyword?(fields) do
compile_error(caller, "expected key-value pairs in struct #{Macro.to_string(name)}")
end
types =
:lists.map(
fn %{field: field} ->
default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
fn
{:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)}
{field, _} -> {field, Keyword.get(fields, field, quote(do: term()))}
end,
struct_info
:lists.sort(struct)
)
fun = fn {field, _} ->
if not Enum.any?(struct_info, &(&1.field == field)) do
unless Keyword.has_key?(struct, field) do
compile_error(
caller,
"undefined field #{inspect(field)} on struct #{inspect(module)}"
@@ -611,7 +596,7 @@ defmodule Kernel.Typespec do
end
:lists.foreach(fun, fields)
typespec({:%{}, meta, [__struct__: module] ++ :lists.usort(types)}, vars, caller, state)
typespec({:%{}, meta, [__struct__: module] ++ types}, vars, caller, state)
_ ->
compile_error(
@@ -645,7 +630,7 @@ defmodule Kernel.Typespec do
)
fun = fn {field, _} ->
if not Keyword.has_key?(fields, field) do
unless Keyword.has_key?(fields, field) do
compile_error(caller, "undefined field #{field} on record #{inspect(tag)}")
end
end
@@ -686,7 +671,14 @@ defmodule Kernel.Typespec do
when is_list(args) do
{args, state} = fn_args(meta, args, vars, caller, state)
{spec, state} = typespec(return, vars, caller, state)
{{:type, location(meta), :fun, [args, spec]}, state}
fun_args =
case [args, spec] do
[{:type, _, :any}, {:type, _, :any, []}] -> []
pair -> pair
end
{{:type, location(meta), :fun, fun_args}, state}
end
# Handle type operator
@@ -762,7 +754,7 @@ defmodule Kernel.Typespec do
) do
remote = Module.get_attribute(caller.module, attr)
if not (is_atom(remote) and remote != nil) do
unless is_atom(remote) and remote != nil do
message =
"invalid remote in typespec: #{Macro.to_string(orig)} (@#{attr} is #{inspect(remote)})"
@@ -956,7 +948,7 @@ defmodule Kernel.Typespec do
## Helpers
# This is a modified backport of Macro.expand/2 because we want to expand
# This is a backport of Macro.expand/2 because we want to expand
# aliases but we don't them to become compile-time references.
defp expand_remote({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
@@ -964,12 +956,9 @@ defmodule Kernel.Typespec do
receiver
[head | tail] ->
case Macro.expand(head, env) do
head when is_atom(head) ->
:elixir_aliases.concat([head | tail])
_ ->
compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}")
case Macro.expand_once(head, env) do
head when is_atom(head) -> :elixir_aliases.concat([head | tail])
_ -> alias
end
end
end
+18 -21
View File
@@ -103,7 +103,7 @@ defmodule Kernel.Utils do
def defstruct(module, fields, bootstrapped?, env) do
{set, bag} = :elixir_module.data_tables(module)
if :ets.member(set, {:elixir, :struct}) do
if :ets.member(set, :__struct__) do
raise ArgumentError,
"defstruct has already been called for " <>
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
@@ -119,8 +119,6 @@ defmodule Kernel.Utils do
mapper = fn
{key, val} when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :none, false)
rescue
@@ -131,7 +129,6 @@ defmodule Kernel.Utils do
end
key when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
{key, nil}
other ->
@@ -166,24 +163,23 @@ defmodule Kernel.Utils do
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
body =
case bootstrapped? do
true ->
case enforce_keys do
[] ->
quote line: 0, generated: true do
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
quote do
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
%{map | key => val}
end)
end
_ ->
quote line: 0, generated: true do
quote do
{map, keys} =
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
{key, val}, {map, keys} ->
{%{map | key => val}, List.delete(keys, key)}
end)
@@ -201,10 +197,10 @@ defmodule Kernel.Utils do
end
false ->
quote line: 0, generated: true do
quote do
:lists.foldl(
fn {key, val}, acc -> %{acc | key => val} end,
unquote(escaped_struct),
@__struct__,
kv
)
end
@@ -212,13 +208,17 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
# The __struct__ attribute is during expansion and for loading remote structs
:ets.insert(set, {:__struct__, struct, nil, []})
# The complete metadata goes into __info__(:struct)
mapper = fn {key, val} ->
%{field: key, default: val}
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
{struct, :lists.reverse(derive), quote(do: kv), body}
error_keys ->
raise ArgumentError,
@@ -330,11 +330,8 @@ defmodule Kernel.Utils do
quote do
case Macro.Env.in_guard?(__CALLER__) do
true ->
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
end
end
end
@@ -399,8 +396,8 @@ defmodule Kernel.Utils do
end
end
defp literal_quote(ast, args) do
{:quote, [], [args, [do: ast]]}
defp literal_quote(ast) do
{:quote, [], [[do: ast]]}
end
defp literal_unquote(ast) do
+1 -1
View File
@@ -6,7 +6,7 @@ defmodule Keyword do
The second element, known as the *value*, can be any term.
Keywords are mostly used to work with optional values. For a general introduction
to keywords and how they compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
to keywords and how the compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
guide.
## Examples
+48 -59
View File
@@ -633,39 +633,27 @@ defmodule List do
[other]
end
@deprecated "Use Enum.zip/1 instead"
# We keep the old implementation because it also supported lists
# of tuples, even though this was not included in its @spec.
@doc """
Zips corresponding elements from each list in `list_of_lists`.
The zipping finishes as soon as any list terminates.
## Examples
iex> List.zip([[1, 2], [3, 4], [5, 6]])
[{1, 3, 5}, {2, 4, 6}]
iex> List.zip([[1, 2], [3], [5, 6]])
[{1, 3, 5}]
"""
@spec zip([list]) :: [tuple]
def zip([]), do: []
def zip(list_of_lists) when is_list(list_of_lists), do: do_zip(list_of_lists, [])
defp do_zip(list, acc) do
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
case :lists.mapfoldl(converter, [], list) do
{_, nil} ->
:lists.reverse(acc)
{mlist, heads} ->
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
end
def zip(list_of_lists) when is_list(list_of_lists) do
do_zip(list_of_lists, [])
end
defp do_zip_each(_, nil) do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
end
defp do_zip_each([], _) do
{nil, nil}
end
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
defp to_list(list) when is_list(list), do: list
@doc ~S"""
Checks if `list` is a charlist made only of printable ASCII characters.
@@ -936,34 +924,6 @@ defmodule List do
def starts_with?(list, []) when is_list(list), do: true
def starts_with?(list, [_ | _]) when is_list(list), do: false
@doc """
Returns `true` if `list` ends with the given `suffix` list, otherwise returns `false`.
If `suffix` is an empty list, it returns `true`.
### Examples
iex> List.ends_with?([1, 2, 3], [2, 3])
true
iex> List.ends_with?([1, 2], [1, 2, 3])
false
iex> List.ends_with?([:alpha], [])
true
iex> List.ends_with?([], [:alpha])
false
"""
@doc since: "1.18.0"
@spec ends_with?(nonempty_list, nonempty_list) :: boolean
@spec ends_with?(list, []) :: true
@spec ends_with?([], nonempty_list) :: false
def ends_with?(list, suffix) do
:lists.suffix(suffix, list)
end
@doc """
Converts a charlist to an atom.
@@ -1133,7 +1093,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) ->
@@ -1185,7 +1145,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) ->
@@ -1412,4 +1372,33 @@ defmodule List do
defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc])
end
# zip
defp do_zip(list, acc) do
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
case :lists.mapfoldl(converter, [], list) do
{_, nil} ->
:lists.reverse(acc)
{mlist, heads} ->
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
end
end
defp do_zip_each(_, nil) do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
end
defp do_zip_each([], _) do
{nil, nil}
end
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
defp to_list(list) when is_list(list), do: list
end
+1 -1
View File
@@ -58,6 +58,6 @@ end
defimpl List.Chars, for: Float do
def to_charlist(term) do
Float.to_charlist(term)
:erlang.float_to_list(term, [:short])
end
end
+60 -263
View File
@@ -146,6 +146,8 @@ defmodule Macro do
* `:delimiter` - contains the opening delimiter for sigils, strings,
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
* `:format` - set to `:keyword` when an atom is defined as a keyword
* `:do` - contains metadata about the `do` location in a function call with
`do`-`end` blocks (when `:token_metadata` is true)
@@ -157,18 +159,10 @@ defmodule Macro do
expressions inside "blocks of code", which are either direct children
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 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`
character (either a newline or `;`)
* `:indentation` - indentation of a sigil heredoc
* `:parens` - denotes a node was surrounded by parens for grouping.
This entry may appear multiple times in the same metadata if
multiple pairs are used for grouping
The following metadata keys are private:
* `:alias` - Used for alias hygiene.
@@ -782,23 +776,23 @@ defmodule Macro do
## Options
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions unescaped, effectively unquoting
* `:unquote` - when true, this function leaves `unquote/1` and
`unquote_splicing/1` statements unescaped, effectively unquoting
the contents on escape. This option is useful only when escaping
ASTs which may have quoted fragments in them. Defaults to `false`.
ASTs which may have quoted fragments in them. Defaults to false.
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
* `:prune_metadata` - when true, removes metadata from escaped AST
nodes. Note this option changes the semantics of escaped code and
it should only be used when escaping ASTs. Defaults to `false`.
it should only be used when escaping ASTs. Defaults to false.
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
assertion, so when an assertion fails we can show code snippets to users.
Without this option, each time the test module is compiled, we would get a
different MD5 of the module bytecode, because the AST contains metadata,
As an example, `ExUnit` stores the AST of every assertion, so when
an assertion fails we can show code snippets to users. Without this
option, each time the test module is compiled, we get a different
MD5 of the module bytecode, because the AST contains metadata,
such as counters, specific to the compilation environment. By pruning
the metadata, we ensure that the module is deterministic and reduce
the amount of data `ExUnit` needs to keep around. Only the minimal
amount of metadata is kept, such as `:line`, `:no_parens` and `:delimiter`.
amount of metadata is kept, such as `:line` and `:no_parens`.
## Comparison to `quote/2`
@@ -820,8 +814,7 @@ defmodule Macro do
iex> value = {:a, :b, :c}
iex> quote do: unquote(value)
** (ArgumentError) tried to unquote invalid AST: {:a, :b, :c}
Did you forget to escape term using Macro.escape/1?
{:a, :b, :c}
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
@@ -834,36 +827,30 @@ defmodule Macro do
:elixir_quote.escape(expr, kind, unquote)
end
# TODO: Deprecate me on Elixir v1.22
@doc deprecated: "Use Macro.struct_info!/2 instead"
def struct!(module, env) when is_atom(module) do
pairs =
for %{field: field, default: default} <- struct_info!(module, env), do: {field, default}
:maps.from_list([__struct__: module] ++ pairs)
end
@doc """
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
Expands the struct given by `module` in the given `env`.
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
Calling this function also adds an export dependency on the
given struct.
It will raise `ArgumentError` if the struct is not available.
It will raise `CompileError` if the struct is not available.
From Elixir v1.12, calling this function also adds an export
dependency on the given struct.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
[%{field: atom(), required: boolean(), default: term()}]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
@doc since: "1.8.0"
@spec struct!(module, Macro.Env.t()) ::
%{required(:__struct__) => module, optional(atom) => any}
when module: module()
def struct!(module, env) when is_atom(module) do
if module == env.module do
Module.get_attribute(module, :__struct__)
end ||
case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do
{:ok, struct} -> struct
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
end
@doc """
@@ -908,8 +895,8 @@ defmodule Macro do
defp find_invalid(bin) when is_binary(bin), do: nil
defp find_invalid(fun) when is_function(fun) do
if Function.info(fun, :env) != {:env, []} or
Function.info(fun, :type) != {:type, :external} do
unless Function.info(fun, :env) == {:env, []} and
Function.info(fun, :type) == {:type, :external} do
{:error, fun}
end
end
@@ -1138,10 +1125,6 @@ defmodule Macro do
`Code.quoted_to_algebra/2` as a lower level function
with more control around formatting.
If the AST contains invalid nodes, this function will
attempt to inspect them, to aid debugging, although
the elements won't be formatted accordingly.
## Examples
iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
@@ -1150,9 +1133,7 @@ defmodule Macro do
"""
@spec to_string(t()) :: String.t()
def to_string(tree) do
doc =
Inspect.Algebra.format(Code.quoted_to_algebra(tree, migrate_charlists_as_sigils: true), 98)
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
IO.iodata_to_binary(doc)
end
@@ -1501,7 +1482,7 @@ defmodule Macro do
:error
end
defp op_call({:..//, _, [left, middle, right]} = ast, fun) do
defp op_call({:"..//", _, [left, middle, right]} = ast, fun) do
left = op_to_string(left, fun, :.., :left)
middle = op_to_string(middle, fun, :.., :right)
right = op_to_string(right, fun, :"//", :right)
@@ -1952,7 +1933,7 @@ defmodule Macro do
@spec operator?(name :: atom(), arity()) :: boolean()
def operator?(name, arity)
def operator?(:..//, 3),
def operator?(:"..//", 3),
do: true
# Code.Identifier treats :// as a binary operator for precedence
@@ -2155,8 +2136,9 @@ defmodule Macro do
@doc """
Converts the given argument to a string with the underscore-slash format.
The argument must either be an atom, representing an Elixir module,
or a string representing a module without the `Elixir.` prefix.
The argument must either be an atom or a string.
If an atom is given, it is assumed to be an Elixir module,
so it is converted to a string and then processed.
This function was designed to format language identifiers/tokens with the underscore-slash format,
that's why it belongs to the `Macro` module. Do not use it as a general
@@ -2190,7 +2172,7 @@ defmodule Macro do
"Foo.Bar"
"""
@spec underscore(atom() | String.t()) :: String.t()
@spec underscore(module() | atom() | String.t()) :: String.t()
def underscore(atom_or_string)
def underscore(atom) when is_atom(atom) do
@@ -2334,15 +2316,6 @@ defmodule Macro do
as a key (`:key`), or as the function name of a remote call
(`:remote_call`).
## Options
* `:escape` - a two-arity function used to escape a quoted
atom content, if necessary. The function receives the atom
content as string and a quote delimiter character, which
should always be escaped. By default the content is escaped
such that the inspected sequence would be parsed as the
given atom.
## Examples
### As a literal
@@ -2391,14 +2364,14 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
def inspect_atom(source_format, atom, opts \\ [])
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
def inspect_atom(source_format, atom)
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
Atom.to_string(atom)
end
def inspect_atom(:literal, atom, opts) when is_atom(atom) do
def inspect_atom(:literal, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2410,7 +2383,7 @@ defmodule Macro do
end
:quoted ->
escaped = inspect_atom_escape(opts, binary, ?")
{escaped, _} = Code.Identifier.escape(binary, ?")
IO.iodata_to_binary([?:, ?", escaped, ?"])
_ ->
@@ -2418,7 +2391,7 @@ defmodule Macro do
end
end
def inspect_atom(:key, atom, opts) when is_atom(atom) do
def inspect_atom(:key, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2426,7 +2399,7 @@ defmodule Macro do
IO.iodata_to_binary([?", binary, ?", ?:])
:quoted ->
escaped = inspect_atom_escape(opts, binary, ?")
{escaped, _} = Code.Identifier.escape(binary, ?")
IO.iodata_to_binary([?", escaped, ?", ?:])
_ ->
@@ -2434,7 +2407,7 @@ defmodule Macro do
end
end
def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do
def inspect_atom(:remote_call, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case inner_classify(atom) do
@@ -2446,22 +2419,13 @@ defmodule Macro do
if type in [:not_callable, :alias] do
binary
else
inspect_atom_escape(opts, binary, ?")
elem(Code.Identifier.escape(binary, ?"), 0)
end
IO.iodata_to_binary([?", escaped, ?"])
end
end
defp inspect_atom_escape(opts, string, char) do
if escape = opts[:escape] do
escape.(string, char)
else
{escaped, _} = Code.Identifier.escape(string, char)
escaped
end
end
# Classifies the given atom into one of the following categories:
#
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
@@ -2488,7 +2452,7 @@ defmodule Macro do
#
defp inner_classify(atom) when is_atom(atom) do
cond do
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] ->
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
:not_callable
# <|>, ^^^, and ~~~ are deprecated
@@ -2586,13 +2550,13 @@ defmodule Macro do
header = dbg_format_header(env)
quote do
to_debug = unquote(dbg_ast_to_debuggable(code, env))
to_debug = unquote(dbg_ast_to_debuggable(code))
unquote(__MODULE__).__dbg__(unquote(header), to_debug, unquote(options))
end
end
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast) do
value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
@@ -2625,7 +2589,7 @@ defmodule Macro do
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
# Logic operators.
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast, _env)
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast)
when op in unquote(dbg_decomposed_binary_operators) do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
@@ -2633,22 +2597,11 @@ defmodule Macro do
quote do
unquote(acc_var) = []
unquote(dbg_boolean_tree(ast, acc_var, result_var))
{:logic_op, Enum.reverse(unquote(acc_var)), unquote(result_var)}
{:logic_op, Enum.reverse(unquote(acc_var))}
end
end
defp dbg_ast_to_debuggable({:__block__, _meta, exprs} = ast, _env) when exprs != [] do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
quote do
unquote(acc_var) = []
unquote(dbg_block(ast, acc_var, result_var))
{:block, Enum.reverse(unquote(acc_var)), unquote(result_var)}
end
end
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast, _env) do
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast) do
clauses_returning_index =
Enum.with_index(clauses, fn {:->, meta, [left, right]}, index ->
{:->, meta, [left, {right, index}]}
@@ -2666,7 +2619,7 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast, _env) do
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast) do
modified_clauses =
Enum.with_index(clauses, fn {:->, _meta, [[left], right]}, index ->
hd(
@@ -2687,104 +2640,8 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:if, meta, [condition_ast, clauses]} = ast, env) do
case Macro.Env.lookup_import(env, {:if, 2}) do
[macro: Kernel] ->
condition_result_var = unique_var(:condition_result, __MODULE__)
quote do
unquote(condition_result_var) = unquote(condition_ast)
result = unquote({:if, meta, [condition_result_var, clauses]})
{:if, unquote(escape(ast)), unquote(escape(condition_ast)),
unquote(condition_result_var), result}
end
_ ->
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
end
defp dbg_ast_to_debuggable({:with, meta, args} = ast, _env) do
{opts, clauses} = List.pop_at(args, -1)
acc_var = unique_var(:acc, __MODULE__)
modified_clauses =
Enum.flat_map(clauses, fn
# We only detail assignments and pattern-matching clauses that
# can be helpful to understand how the result is constructed.
{:<-, _meta, [left, right]} ->
modified_left =
case left do
{:when, meta, [pattern, guard]} -> {:when, meta, [{pattern, quote(do: _)}, guard]}
pattern -> {pattern, quote(do: _)}
end
quote do
[
value = unquote(right),
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
unquote(modified_left) <- {value, unquote(acc_var)}
]
end
{:=, _meta, [left, right]} ->
quote do
[
value = unquote(right),
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
unquote(left) = value
]
end
# Other expressions like side effects are omitted.
expr ->
[expr]
end)
modified_opts =
Enum.map(opts, fn
{:do, do_block} ->
{:do, {do_block, acc_var}}
{:else, else_block} ->
clauses =
Enum.map(else_block, fn
{:->, meta, [[{:when, meta2, [pattern, guard]}], right]} ->
{:->, meta, [[{:when, meta2, [{pattern, acc_var}, guard]}], {right, acc_var}]}
{:->, meta, [[left], right]} ->
{:->, meta, [[{left, acc_var}], {right, acc_var}]}
invalid ->
invalid
end)
error_clause =
quote generated: true do
{other, _acc} -> raise WithClauseError, term: other
end
{:else, clauses ++ error_clause}
invalid ->
invalid
end)
modified_with_ast = {:with, meta, modified_clauses ++ [modified_opts]}
quote do
unquote(acc_var) = []
{value, acc} = unquote(modified_with_ast)
{:with, unquote(escape(ast)), Enum.reverse(acc), value}
end
end
# Any other AST.
defp dbg_ast_to_debuggable(ast, _env) do
defp dbg_ast_to_debuggable(ast) do
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
@@ -2815,18 +2672,6 @@ defmodule Macro do
end
end
defp dbg_block({:__block__, meta, exprs}, acc_var, result_var) do
modified_exprs =
Enum.map(exprs, fn expr ->
quote do
unquote(result_var) = unquote(expr)
unquote(acc_var) = [{unquote(escape(expr)), unquote(result_var)} | unquote(acc_var)]
end
end)
{:__block__, meta, modified_exprs}
end
# Made public to be called from Macro.dbg/3, so that we generate as little code
# as possible and call out into a function as soon as we can.
@doc false
@@ -2864,27 +2709,15 @@ defmodule Macro do
{[first_formatted | rest_formatted], result}
end
defp dbg_format_ast_to_debug({:logic_op, components, value}, options) do
defp dbg_format_ast_to_debug({:logic_op, components}, options) do
{_ast, final_value} = List.last(components)
formatted =
Enum.map(components, fn {ast, value} ->
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
end)
{formatted, value}
end
defp dbg_format_ast_to_debug({:block, components, value}, options) do
formatted =
[
dbg_maybe_underline("Code block", options),
":\n(\n",
Enum.map(components, fn {ast, value} ->
[" ", dbg_format_ast_with_value(ast, value, options)]
end),
")\n"
]
{formatted, value}
{formatted, final_value}
end
defp dbg_format_ast_to_debug({:case, ast, expr_value, clause_index, value}, options) do
@@ -2920,42 +2753,6 @@ defmodule Macro do
{formatted, value}
end
defp dbg_format_ast_to_debug(
{:if, ast, condition_ast, condition_result, result},
options
) do
formatted = [
dbg_maybe_underline("If condition", options),
":\n",
dbg_format_ast_with_value(condition_ast, condition_result, options),
?\n,
dbg_maybe_underline("If expression", options),
":\n",
dbg_format_ast_with_value(ast, result, options)
]
{formatted, result}
end
defp dbg_format_ast_to_debug({:with, ast, clauses, result}, options) do
formatted_clauses =
Enum.map(clauses, fn {clause_ast, clause_result} ->
dbg_format_ast_with_value(clause_ast, clause_result, options)
end)
formatted = [
dbg_maybe_underline("With clauses", options),
":\n",
formatted_clauses,
?\n,
dbg_maybe_underline("With expression", options),
":\n",
dbg_format_ast_with_value(ast, result, options)
]
{formatted, result}
end
defp dbg_format_ast_to_debug({:value, code_ast, value}, options) do
{dbg_format_ast_with_value(code_ast, value, options), value}
end
+3 -5
View File
@@ -102,10 +102,8 @@ defmodule Macro.Env do
]
# Define the __struct__ callbacks by hand for bootstrap reasons.
{_struct, [], escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(escaped_struct)
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(:elixir_quote.escape(struct, :none, false))
def __struct__(unquote(kv)), do: unquote(body)
@doc """
@@ -445,7 +443,7 @@ defmodule Macro.Env do
maybe_define_error(result, :elixir_aliases)
end
defp maybe_define_error({:ok, _info, env}, _mod),
defp maybe_define_error({:ok, env}, _mod),
do: {:ok, env}
defp maybe_define_error({:error, reason}, mod),
+16 -13
View File
@@ -96,11 +96,11 @@ defmodule Map do
iex> %{map | one: "one"}
%{one: "one", two: 2}
Or any other keys:
Or any other key:
iex> other_map = %{"three" => 3, "four" => 4, "five" => 5}
iex> %{other_map | "three" => "three", "four" => "four"}
%{"five" => 5, "four" => "four", "three" => "three"}
iex> other_map = %{"three" => 3, "four" => 4}
iex> %{other_map | "three" => "three"}
%{"four" => 4, "three" => "three"}
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
@@ -128,8 +128,6 @@ defmodule Map do
@doc """
Builds a map from the given `keys` and the fixed `value`.
Inlined by the compiler.
## Examples
iex> Map.from_keys([1, 2, 3], :number)
@@ -138,7 +136,9 @@ defmodule Map do
"""
@doc since: "1.14.0"
@spec from_keys([key], value) :: map
defdelegate from_keys(keys, value), to: :maps
def from_keys(keys, value) do
:maps.from_keys(keys, value)
end
@doc """
Returns all keys from `map`.
@@ -709,10 +709,10 @@ defmodule Map do
end
@doc """
Removes and returns the value associated with `key` in `map` alongside
the updated map, or raises if `key` is not present.
Removes the value associated with `key` in `map` and returns the value
and the updated map, or it raises if `key` is not present.
Behaves the same as `pop/3` but raises a `KeyError` exception if `key` is not present in `map`.
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
## Examples
@@ -1011,8 +1011,9 @@ defmodule Map do
@doc """
Converts a `struct` to map.
It accepts a struct and simply removes the `__struct__` field
from the given struct.
It accepts the struct module or a struct itself and
simply removes the `__struct__` field from the given struct
or from a new struct generated from the given module.
## Example
@@ -1020,13 +1021,15 @@ defmodule Map do
defstruct [:name]
end
Map.from_struct(User)
#=> %{name: nil}
Map.from_struct(%User{name: "john"})
#=> %{name: "john"}
"""
@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")
delete(struct.__struct__(), :__struct__)
end
+1 -1
View File
@@ -401,7 +401,7 @@ defmodule MapSet do
"""
@doc since: "1.15.0"
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
@spec split_with(MapSet.t(), (any() -> 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}}
+71 -108
View File
@@ -270,9 +270,6 @@ defmodule Module do
a dependency on it, causing the module to be recompiled as soon
as the file is added.
For more control over when a module is recompiled, see
[`__mix_recompile__?/0`](`m:Mix.Tasks.Compile.Elixir#module-__mix_recompile__-0`).
### `@file`
Changes the filename used in stacktraces for the function or macro that
@@ -444,8 +441,7 @@ defmodule Module do
* `@typep` - defines a private type to be used in `@spec`
* `@opaque` - defines an opaque type to be used in `@spec`
* `@spec` - provides a specification for a function
* `@callback` - provides a specification for a behaviour callback (and generates
a `behaviour_info/1` function in the module, see below)
* `@callback` - provides a specification for a behaviour callback
* `@macrocallback` - provides a specification for a macro behaviour callback
* `@optional_callbacks` - specifies which behaviour callbacks and macro
behaviour callbacks are optional
@@ -594,78 +590,6 @@ defmodule Module do
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
the given module or the given `Mod.fun/arity` are not defined
## Generated functions
Sometimes the compiler will generate public functions within modules. These
are documented below.
### `behaviour_info/1`
This function is generated for modules that define a behaviour, that is,
that have one or more `@callback` definitions. The signature for this function,
expressed as a spec, is:
@spec behaviour_info(:callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec behaviour_info(:optional_callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
`behaviour_info(:callbacks)` includes optional callbacks.
For example:
iex> Enum.sort(GenServer.behaviour_info(:callbacks))
[
code_change: 3,
format_status: 1,
format_status: 2,
handle_call: 3,
handle_cast: 2,
handle_continue: 2,
handle_info: 2,
init: 1,
terminate: 2
]
### `module_info/0`
This function is generated for all modules. It returns all the attributes
returned by `module_info/1` (see below), but as a single keyword list. See also the
[Erlang documentation](https://www.erlang.org/doc/system/modules.html#module_info-0).
### `module_info/1`
This function is generated for all modules and returns
information about the module. The signature for this function,
expressed as a spec, is:
@spec module_info(:module) :: module() # Returns the module itself
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@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]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
For example:
iex> URI.module_info(:module)
URI
iex> {:decode_www_form, 1} in URI.module_info(:exports)
true
For more information about `module_info/1`, also check out the [Erlang
documentation](https://www.erlang.org/doc/system/modules.html#module_info-1).
### `__info__/1`
This function is generated for all modules. It's similar to `module_info/1` but
includes some additional Elixir-specific information, such as struct and macro
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@@ -704,8 +628,7 @@ defmodule Module do
@callback __info__(:macros) :: keyword()
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
@callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil
@doc """
Returns information about module attributes used by Elixir.
@@ -746,10 +669,6 @@ defmodule Module do
doc:
"If set to `true` generates a default protocol implementation for all types (inside `defprotocol`)."
},
undefined_impl_description: %{
doc:
"A string with additional description to be used on `Protocol.UndefinedError` when looking up the implementation fails."
},
for: %{
doc:
"The current module/type a protocol implementation is being defined for (inside `defimpl`)."
@@ -834,7 +753,50 @@ defmodule Module do
:elixir_module.is_open(module)
end
@deprecated "Use Code.eval_quoted/3 instead"
@doc """
Evaluates the quoted contents in the given module's context.
A list of environment options can also be given as argument.
See `Code.eval_string/3` for more information.
Raises an error if the module was already compiled.
## Examples
defmodule Foo do
contents =
quote do
def sum(a, b), do: a + b
end
Module.eval_quoted(__MODULE__, contents)
end
Foo.sum(1, 2)
#=> 3
For convenience, you can pass any `Macro.Env` struct, such
as `__ENV__/0`, as the first argument or as options. Both
the module and all options will be automatically extracted
from the environment:
defmodule Foo do
contents =
quote do
def sum(a, b), do: a + b
end
Module.eval_quoted(__ENV__, contents)
end
Foo.sum(1, 2)
#=> 3
Note that if you pass a `Macro.Env` struct as first argument
while also passing `opts`, they will be merged with `opts`
having precedence.
"""
@spec eval_quoted(module | Macro.Env.t(), Macro.t(), list, keyword | Macro.Env.t()) :: term
def eval_quoted(module_or_env, quoted, binding \\ [], opts \\ [])
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts)
@@ -865,8 +827,7 @@ defmodule Module do
the given quoted expressions.
The line where the module is defined and its file **must**
be passed as options. See `Code.env_for_eval/1` for a complete
list of options.
be passed as options.
It returns a tuple of shape `{:module, module, binary, term}`
where `module` is the module name, `binary` is the module
@@ -910,7 +871,7 @@ defmodule Module do
end
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
if not Keyword.has_key?(opts, :file) do
unless Keyword.has_key?(opts, :file) do
raise ArgumentError, "expected :file to be given as option"
end
@@ -921,7 +882,7 @@ defmodule Module do
defp create(meta, module, quoted, env_or_opts) do
next = :elixir_module.next_counter(nil)
quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted)
:elixir_module.compile(meta, module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
:elixir_module.compile(module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
end
@doc """
@@ -946,8 +907,7 @@ defmodule Module do
@doc """
Concatenates two aliases and returns a new alias.
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
It handles binaries and atoms.
## Examples
@@ -957,9 +917,6 @@ defmodule Module do
iex> Module.concat(Foo, "Bar")
Foo.Bar
iex> Module.concat(Foo, nil)
Foo
"""
@spec concat(binary | atom, binary | atom) :: atom
def concat(left, right)
@@ -1424,7 +1381,15 @@ defmodule Module do
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
assert_not_readonly!(__ENV__.function, module)
:elixir_def.take_definition(module, {name, arity}) != false
case :elixir_def.take_definition(module, {name, arity}) do
false ->
false
_ ->
:elixir_locals.yank({name, arity}, module)
true
end
end
@doc """
@@ -1453,7 +1418,8 @@ defmodule Module do
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, tuple, clause)
neighbours = :elixir_locals.yank(tuple, module)
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
end
other ->
@@ -1677,11 +1643,11 @@ defmodule Module do
case :ets.lookup(set, key) do
[{_, _, :accumulate, traces}] ->
trace_attribute(true, module, traces, set, key, nil, [])
trace_attribute(true, module, traces, set, key, [])
reverse_values(:ets.take(bag, {:accumulate, key}), [])
[{_, value, _, traces}] ->
trace_attribute(module, key, nil, traces)
trace_attribute(module, traces)
:ets.delete(set, key)
value
@@ -1944,7 +1910,7 @@ defmodule Module do
_ ->
message =
case :ets.lookup(set, {:elixir, :struct}) do
case :ets.lookup(set, :__struct__) do
[] ->
"warning: module attribute @derive was set but never used (it must come before defstruct)"
@@ -1998,15 +1964,15 @@ defmodule Module do
default
[{_, _, :accumulate, traces}] ->
trace_attribute(trace?, module, traces, set, key, caller_line, [])
trace_attribute(trace?, module, traces, set, key, [])
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
[{_, value, warn_line, traces}] when is_integer(warn_line) ->
trace_attribute(trace?, module, traces, set, key, caller_line, [{3, :used}])
trace_attribute(trace?, module, traces, set, key, [{3, :used}])
value
[{_, value, _, traces}] ->
trace_attribute(trace?, module, traces, set, key, caller_line, [])
trace_attribute(trace?, module, traces, set, key, [])
value
[] when is_integer(caller_line) ->
@@ -2034,15 +2000,12 @@ defmodule Module do
end
end
defp trace_attribute(module, key, caller_line, traces) do
defp trace_attribute(module, traces) do
:lists.foreach(
fn {line, lexical_tracker, tracers, aliases} ->
line = caller_line || line
env = %{
Macro.Env.__struct__()
| line: line,
file: "@#{key}",
lexical_tracker: lexical_tracker,
module: module,
tracers: tracers
@@ -2059,10 +2022,10 @@ defmodule Module do
)
end
defp trace_attribute(trace?, module, traces, set, key, caller_line, updates) do
defp trace_attribute(trace?, module, traces, set, key, updates) do
updates =
if trace? and traces != [] do
trace_attribute(module, key, caller_line, traces)
trace_attribute(module, traces)
updates ++ [{4, []}]
else
updates
@@ -2266,7 +2229,7 @@ defmodule Module do
end
defp preprocess_attribute(:nifs, value) do
if not function_arity_list?(value) do
unless function_arity_list?(value) do
raise ArgumentError,
"@nifs is a built-in module attribute for specifying a list " <>
"of functions and their arities that are NIFs, got: #{inspect(value)}"
+13 -4
View File
@@ -4,7 +4,7 @@ defmodule Module.Behaviour do
@doc false
def callbacks(behaviour) do
for callback <- behaviour.behaviour_info(:callbacks) do
for callback <- behaviour_info(behaviour, :callbacks) do
{pair, _kind} = normalize_macro_or_function_callback(callback)
pair
end
@@ -33,7 +33,9 @@ defmodule Module.Behaviour do
module: module,
file: file,
line: line,
# Map containing the callbacks to be implemented
callbacks: %{},
# list of warnings {message, env}
warnings: []
}
end
@@ -55,8 +57,8 @@ defmodule Module.Behaviour do
warn(context, {:module_does_not_define_behaviour, context.module, behaviour})
true ->
optional_callbacks = behaviour.behaviour_info(:optional_callbacks)
callbacks = behaviour.behaviour_info(:callbacks)
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
callbacks = behaviour_info(behaviour, :callbacks)
Enum.reduce(callbacks, context, &add_callback(&2, &1, behaviour, optional_callbacks))
end
end)
@@ -142,7 +144,7 @@ defmodule Module.Behaviour do
end
defp impl_behaviours({function, arity}, defaults, kind, value, behaviours, callbacks) do
impls = for n <- (arity - defaults)..arity, do: {function, n}
impls = for n <- arity..(arity - defaults), do: {function, n}
impl_behaviours(impls, kind, value, behaviours, callbacks)
end
@@ -264,6 +266,13 @@ defmodule Module.Behaviour do
". The known callbacks are:\n#{formatted_callbacks}\n"
end
defp behaviour_info(module, key) do
case module.behaviour_info(key) do
list when is_list(list) -> list
:undefined -> []
end
end
defp normalize_macro_or_function_callback({function_name, arity}) do
case :erlang.atom_to_list(function_name) do
# Macros are always provided one extra argument in behaviour_info/1
+258
View File
@@ -0,0 +1,258 @@
# This is an Elixir module responsible for tracking
# calls in order to extract Elixir modules' behaviour
# during compilation time.
#
# ## Implementation
#
# The implementation uses ETS to track all dependencies
# resembling a graph. The keys and what they point to are:
#
# * `:reattach` points to `{name, arity}`
# * `{:local, {name, arity}}` points to `{{name, arity}, line, macro_dispatch?}`
# * `{:import, {name, arity}}` points to `Module`
#
# This is built on top of the internal module tables.
defmodule Module.LocalsTracker do
@moduledoc false
@defmacros [:defmacro, :defmacrop]
@doc """
Adds and tracks defaults for a definition into the tracker.
"""
def add_defaults({_set, bag}, kind, {name, arity} = pair, defaults, meta) do
for i <- :lists.seq(arity - defaults, arity - 1) do
put_edge(bag, {:local, {name, i}}, {pair, get_line(meta), kind in @defmacros})
end
:ok
end
@doc """
Adds a local dispatch from-to the given target.
"""
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
put_edge(bag, {:local, from}, {to, get_position(meta), macro_dispatch?})
:ok
end
@doc """
Adds an import dispatch to the given target.
"""
def add_import({set, _bag}, function, module, imported)
when is_tuple(function) and is_atom(module) do
put_edge(set, {:import, imported}, module)
:ok
end
@doc """
Yanks a local node. Returns its in and out vertices in a tuple.
"""
def yank({_set, bag}, local) do
:lists.usort(take_out_neighbours(bag, {:local, local}))
end
@doc """
Reattach a previously yanked node.
"""
def reattach({_set, bag}, tuple, kind, function, out_neighbours, meta) do
for out_neighbour <- out_neighbours do
put_edge(bag, {:local, function}, out_neighbour)
end
# Make a call from the old function to the new one
if function != tuple do
put_edge(bag, {:local, function}, {tuple, get_line(meta), kind in @defmacros})
end
# Finally marked the new one as reattached
put_edge(bag, :reattach, tuple)
:ok
end
@doc """
Collect all conflicting imports with the given functions
"""
def collect_imports_conflicts({set, _bag}, all_defined) do
for {pair, _, meta, _} <- all_defined, n = out_neighbour(set, {:import, pair}) do
{meta, {n, pair}}
end
end
@doc """
Collect all unused definitions based on the private
given, also accounting the expected number of default
clauses a private function have.
"""
def collect_unused_locals({_set, bag}, all_defined, private) do
reachable =
Enum.reduce(all_defined, %{}, fn {pair, kind, _, _}, acc ->
if kind in [:def, :defmacro] do
reachable_from(bag, pair, acc)
else
acc
end
end)
reattached = :lists.usort(out_neighbours(bag, :reattach))
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
end
@doc """
Collect undefined functions based on local calls and existing definitions.
"""
def collect_undefined_locals({set, bag}, all_defined, file) do
undefined =
for {pair, _, meta, _} <- all_defined,
{{local_name, _} = local, position, macro_dispatch?} <-
out_neighbours(bag, {:local, pair}),
error = undefined_local_error(set, local, macro_dispatch?) do
file =
case Keyword.get(meta, :file) do
{keep_file, _keep_line} -> keep_file
nil -> file
end
meta = build_meta(position, local_name)
{pair, meta, file, local, error}
end
:lists.usort(undefined)
end
defp undefined_local_error(set, local, true) do
case :ets.member(set, {:def, local}) do
true -> false
false -> :undefined_function
end
end
defp undefined_local_error(set, local, false) do
try do
if :ets.lookup_element(set, {:def, local}, 2) in @defmacros do
:incorrect_dispatch
else
false
end
catch
_, _ -> :undefined_function
end
end
defp unreachable(reachable, reattached, private) do
for {tuple, kind, _, _} <- private,
not reachable?(tuple, kind, reachable, reattached),
do: tuple
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private macros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
end
defp reachable?(tuple, :defp, reachable, _reattached) do
Map.has_key?(reachable, tuple)
end
defp collect_warnings(reachable, private) do
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
end
defp collect_warnings({_, _, false, _}, acc, _reachable) do
acc
end
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
if Map.has_key?(reachable, tuple) do
acc
else
[{meta, {:unused_def, tuple, kind}} | acc]
end
end
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, reachable) do
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
^min -> acc
^max -> [{meta, {:unused_args, tuple}} | acc]
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
end
end
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
case Map.has_key?(reachable, {name, max}) do
true -> min_reachable_default(max - 1, min, max, name, reachable)
false -> min_reachable_default(max - 1, min, last, name, reachable)
end
end
defp min_reachable_default(_max, _min, last, _name, _reachable) do
last
end
@doc """
Returns all local nodes reachable from `vertex`.
By default, all public functions are reachable.
A private function is only reachable if it has
a public function that it invokes directly.
"""
def reachable_from({_, bag}, local) do
bag
|> reachable_from(local, %{})
|> Map.keys()
end
defp reachable_from(bag, local, vertices) do
vertices = Map.put(vertices, local, true)
Enum.reduce(out_neighbours(bag, {:local, local}), vertices, fn {local, _line, _}, acc ->
case acc do
%{^local => true} -> acc
_ -> reachable_from(bag, local, acc)
end
end)
end
defp get_line(meta), do: Keyword.get(meta, :line)
defp get_position(meta), do: {get_line(meta), meta[:column]}
defp build_meta(position, function_name) do
case position do
{line, nil} -> [line: line]
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
end
end
## Lightweight digraph implementation
defp put_edge(d, from, to) do
:ets.insert(d, {from, to})
end
defp out_neighbour(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> nil
end
end
defp out_neighbours(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> []
end
end
defp take_out_neighbours(d, from) do
Keyword.values(:ets.take(d, from))
end
end
+158 -135
View File
@@ -5,19 +5,20 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type kind() :: :def | :defmacro
@type mode() :: :elixir | :erlang
@doc """
Initializes the parallel checker process.
"""
def start_link(schedulers \\ nil) do
:proc_lib.start_link(__MODULE__, :init, [schedulers])
:gen_server.start_link(__MODULE__, schedulers, [])
end
@doc """
Stops the parallel checker process.
"""
def stop({checker, _table}) do
def stop(checker) do
send(checker, {__MODULE__, :stop})
:ok
end
@@ -28,30 +29,26 @@ defmodule Module.ParallelChecker do
def get do
case :erlang.get(:elixir_checker_info) do
{parent, nil} ->
{:ok, cache} = start_link()
put(parent, cache)
{parent, cache}
{:ok, checker} = start_link()
put(parent, checker)
{parent, checker}
{parent, cache} ->
{parent, cache}
{parent, checker} ->
{parent, checker}
end
end
@doc """
Stores the parallel checker information.
"""
def put(pid, {checker, table}) when is_pid(pid) do
:erlang.put(:elixir_checker_info, {pid, {checker, table}})
def put(pid, checker) when is_pid(pid) and is_pid(checker) do
:erlang.put(:elixir_checker_info, {pid, checker})
end
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, {checker, table}}, module, module_map, log?) do
inner_spawn(pid, checker, table, module, cache_from_module_map(table, module_map), log?)
end
defp inner_spawn(pid, checker, table, module, info, log?) do
def spawn({pid, checker}, module, info, log?) do
ref = make_ref()
spawned =
@@ -59,25 +56,20 @@ defmodule Module.ParallelChecker do
mon_ref = Process.monitor(pid)
receive do
{^ref, :cache} ->
{^ref, :cache, ets} ->
Process.link(pid)
module_tuple =
cond do
is_tuple(info) ->
info
is_binary(info) ->
with {:ok, binary} <- File.read(info),
{: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
_ -> nil
end
module_map =
if is_map(info) do
info
else
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
end
module_map && cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
@@ -86,8 +78,8 @@ defmodule Module.ParallelChecker do
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
if module_map do
check_module(module_map, {checker, ets}, log?)
else
[]
end
@@ -121,12 +113,12 @@ defmodule Module.ParallelChecker do
case :erlang.get(:elixir_checker_info) do
{_, nil} -> :ok
{_, cache} -> verify(cache, [])
{_, checker} -> verify(checker, [])
end
result
after
{_, cache} = :erlang.get(:elixir_checker_info)
{_, checker} = :erlang.get(:elixir_checker_info)
if previous != :undefined do
:erlang.put(:elixir_checker_info, previous)
@@ -134,7 +126,7 @@ defmodule Module.ParallelChecker do
:erlang.erase(:elixir_checker_info)
end
cache && stop(cache)
checker && stop(checker)
end
_ ->
@@ -149,13 +141,13 @@ defmodule Module.ParallelChecker do
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()]
def verify({checker, table}, runtime_files) do
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
def verify(checker, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
inner_spawn(self(), checker, table, module, file, log?)
spawn({self(), checker}, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -189,8 +181,20 @@ defmodule Module.ParallelChecker do
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
{:ok, checker} = start_link()
{checker, :gen_server.call(checker, :ets, :infinity)}
end
@doc """
Preloads a module into the cache. Call this function before any other
cache lookups for the module.
"""
@spec preload_module(cache(), module()) :: :ok
def preload_module({server, ets}, module) do
case :ets.lookup(ets, {:cached, module}) do
[{_key, _}] -> :ok
[] -> cache_module({server, ets}, module)
end
end
@doc """
@@ -199,37 +203,58 @@ defmodule Module.ParallelChecker do
or if the function does not exist return `{:error, :function}`.
"""
@spec fetch_export(cache(), module(), atom(), arity()) ::
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
| {:error, :function | :module}
def fetch_export({checker, table}, module, fun, arity) do
case :ets.lookup(table, module) do
[] ->
cache_module({checker, table}, module)
fetch_export({checker, table}, module, fun, arity)
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
def fetch_export({_server, ets}, module, fun, arity) do
case :ets.lookup(ets, {:cached, module}) do
[{_key, false}] ->
{:error, :module}
[{_key, mode}] ->
case :ets.lookup(table, {module, {fun, arity}}) do
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
case :ets.lookup(ets, {:export, module, {fun, arity}}) do
[{_key, kind, reason}] -> {:ok, mode, kind, reason}
[] -> {:error, :function}
end
end
end
@doc """
Returns all exported functions and macros for the given module from
the cache.
"""
@spec all_exports(cache(), module()) :: [{atom(), arity()}]
def all_exports({_server, ets}, module) do
# This is only called after we get a deprecation notice
# so we can assume it's a cached module
ets
|> :ets.match({{:export, module, :"$1"}, :_, :_})
|> Enum.flat_map(& &1)
|> Enum.sort()
end
## Module checking
defp check_module(module_tuple, cache, log?) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
module_tuple
defp check_module(module_map, cache, log?) do
%{
module: module,
file: file,
line: line,
compile_opts: compile_opts,
definitions: definitions,
uses_behaviours: uses_behaviours,
impls: impls
} = module_map
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
behaviour_warnings =
Module.Behaviour.check_behaviours_and_impls(
module,
file,
line,
behaviours,
uses_behaviours,
impls,
definitions
)
@@ -239,43 +264,15 @@ defmodule Module.ParallelChecker do
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(file, log?)
|> emit_warnings(log?)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
module_map
|> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
diagnostics
end
defp module_map_to_module_tuple(module_map) do
%{
module: module,
file: file,
compile_opts: compile_opts,
definitions: definitions,
attributes: attributes,
impls: impls,
after_verify: after_verify
} = module_map
# TODO: Match on anno directly in Elixir v1.22+
line =
case module_map do
%{anno: anno} -> :erl_anno.line(anno)
%{line: line} -> line
end
behaviours = for {:behaviour, module} <- attributes, do: module
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
end
defp extract_no_warn_undefined(compile_opts) do
for(
{:no_warn_undefined, values} <- compile_opts,
@@ -320,9 +317,9 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped)
end
defp emit_warnings(warnings, file, log?) do
defp emit_warnings(warnings, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1))
log? and print_diagnostics(diagnostics)
diagnostics
end)
@@ -336,12 +333,10 @@ defmodule Module.ParallelChecker do
:elixir_errors.print_diagnostics(diagnostics)
end
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
file = Path.absname(file)
defp to_diagnostic(diagnostic, {file, position, mfa}) when is_list(position) do
%{
severity: :warning,
source: source,
source: file,
file: file,
position: position_to_tuple(position),
stacktrace: [to_stacktrace(file, position, mfa)],
@@ -372,15 +367,15 @@ defmodule Module.ParallelChecker do
## Cache
defp cache_module({checker, table}, module) do
if lock(checker, module) do
defp cache_module({server, ets}, module) do
if lock(server, module) do
object_code = :code.get_object_code(module)
# The chunk has more information, so that's our preference
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
cache_chunk(table, module, contents.exports)
cache_chunk(ets, module, contents.exports)
else
_ ->
# Otherwise, if the module is loaded, use its info
@@ -388,28 +383,37 @@ defmodule Module.ParallelChecker do
true ->
{mode, exports} = info_exports(module)
deprecated = info_deprecated(module)
cache_info(table, module, exports, deprecated, %{}, mode)
cache_info(ets, module, exports, deprecated, mode)
false ->
# Or load exports from chunk
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
cache_info(table, module, exports, %{}, %{}, :erlang)
exports = Map.new(Enum.map(exports, &{&1, :def}))
cache_info(ets, module, exports, %{}, :erlang)
else
_ ->
:ets.insert(table, {module, false})
:ets.insert(ets, {{:cached, module}, false})
end
end
end
unlock(checker, module)
unlock(server, module)
end
end
defp info_exports(module) do
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
map =
Map.new(
[{{:__info__, 1}, :def}] ++
behaviour_exports(module) ++
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
Enum.map(module.__info__(:functions), &{&1, :def})
)
{:elixir, map}
rescue
_ -> {:erlang, module.module_info(:exports)}
_ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
end
defp info_deprecated(module) do
@@ -418,55 +422,70 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
defp cache_from_module_map(table, map) do
defp maybe_module_map(binary, module) when is_binary(binary) do
# If a module was compiled without debug_info,
# then there is no module_map for further verification.
with {: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
module_map
else
_ -> nil
end
end
defp cache_from_module_map(ets, map) do
exports =
[{:__info__, 1}] ++
[{{:__info__, 1}, :def}] ++
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
definitions_to_exports(map.definitions)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
module_map_to_module_tuple(map)
deprecated = Map.new(map.deprecated)
cache_info(ets, map.module, exports, deprecated, :elixir)
end
defp cache_info(table, module, exports, deprecated, sigs, mode) do
Enum.each(exports, fn fa ->
reason = Map.get(deprecated, fa)
:ets.insert(table, {{module, fa}, reason, Map.get(sigs, fa, :none)})
defp cache_info(ets, module, exports, deprecated, mode) do
Enum.each(exports, fn {{fun, arity}, kind} ->
reason = Map.get(deprecated, {fun, arity})
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
{{fun, arity}, kind}
end)
:ets.insert(table, {module, mode})
:ets.insert(ets, {{:cached, module}, mode})
end
defp cache_chunk(table, module, exports) do
Enum.each(exports, fn {{fun, arity}, info} ->
sig =
case info do
%{sig: {:strong, _, _} = sig} -> sig
%{sig: {:infer, _} = sig} -> sig
_ -> :none
end
defp cache_chunk(ets, module, exports) do
Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
:ets.insert(
table,
{{module, {fun, arity}}, Map.get(info, :deprecated), sig}
)
{{fun, arity}, kind}
end)
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
:ets.insert(table, {module, :elixir})
:ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
:ets.insert(ets, {{:cached, module}, :elixir})
end
defp behaviour_exports(%{defines_behaviour: true}), do: [{:behaviour_info, 1}]
defp behaviour_exports(%{defines_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
defp behaviour_exports(%{defines_behaviour: false}), do: []
defp behaviour_exports(module) when is_atom(module) do
if function_exported?(module, :behaviour_info, 1) do
[{:behaviour_info, 1}]
[{{:behaviour_info, 1}, :def}]
else
[]
end
end
defp definitions_to_exports(definitions) do
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
if kind in [:def, :defmacro] do
[{function, kind}]
else
[]
end
end)
end
defp lock(server, module) do
:gen_server.call(server, {:lock, module}, :infinity)
end
@@ -482,22 +501,22 @@ defmodule Module.ParallelChecker do
## Server callbacks
def init(schedulers) do
table = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
:proc_lib.init_ack({:ok, {self(), table}})
ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
state = %{
ets: ets,
waiting: %{},
modules: [],
spawned: 0,
schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
}
:gen_server.enter_loop(__MODULE__, [], state)
{:ok, state}
end
def handle_call(:start, _from, %{modules: modules} = state) do
def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
for {pid, ref} <- modules do
send(pid, {ref, :cache})
send(pid, {ref, :cache, ets})
end
for {_pid, ref} <- modules do
@@ -509,10 +528,14 @@ defmodule Module.ParallelChecker do
{:reply, length(modules), run_checkers(state)}
end
def handle_call(:ets, _from, state) do
{:reply, state.ets, state}
end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
case waiting do
%{^module => from_list} ->
waiting = Map.put(state.waiting, module, [from | from_list])
%{^module => froms} ->
waiting = Map.put(state.waiting, module, [from | froms])
{:noreply, %{state | waiting: waiting}}
%{} ->
@@ -522,8 +545,8 @@ defmodule Module.ParallelChecker do
end
def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
from_list = Map.fetch!(waiting, module)
Enum.each(from_list, &:gen_server.reply(&1, false))
froms = Map.fetch!(waiting, module)
Enum.each(froms, &:gen_server.reply(&1, false))
waiting = Map.delete(waiting, module)
{:reply, :ok, %{state | waiting: waiting}}
end
+39 -347
View File
@@ -1,304 +1,48 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
# perform subtyping and return its output (OUT).
#
# * :strict - Requires types signatures (not implemented).
# * Strong arrows with gradual performs subtyping and returns OUT
# * Weak arrows with gradual performs subtyping and returns OUT
#
# * :static - Type signatures have been given.
# * Strong arrows with gradual performs compatibility and returns OUT
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :dynamic - Type signatures have not been given.
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :infer - Same as :dynamic but skips remote calls.
#
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
@modes [:static, :dynamic, :infer, :traversal]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [__protocol__: 1, behaviour_info: 1]
@doc false
def infer(module, file, defs, private, used_private, env, {_, cache}) do
infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
false -> false
end
end
handler = fn meta, fun_arity, stack, context ->
case local_handler(meta, fun_arity, stack, context, finder) do
false ->
undefined_function!(:undefined_function, meta, fun_arity, stack, env)
false
{kind, _, _} = triplet ->
if (kind == :defmacro or kind == :defmacrop) and not Keyword.has_key?(meta, :super) do
undefined_function!(:incorrect_dispatch, meta, fun_arity, stack, env)
false
else
triplet
end
end
end
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
{types, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- defs,
kind in [:def, :defmacro],
reduce: {[], context()} do
{types, context} ->
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], context}
else
{types, context}
end
end
# Now traverse all used privates to find any other private that have been used by them.
context =
%{local_sigs: used_sigs} =
for fun_arity <- used_private, reduce: context do
context ->
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
context
end
{unreachable, _context} =
Enum.reduce(private, {[], context}, fn
{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)
# defp is reachable if used, defmacrop only if directly invoked
private_sigs = if kind == :defp, do: used_sigs, else: reachable_sigs
if is_map_key(private_sigs, fun_arity) do
{unreachable, context}
else
{[fun_arity | unreachable], context}
end
end)
{Map.new(types), unreachable}
end
defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
end
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
env = %{env | function: stack.function, file: stack.file}
tuple = {reason, {fun, arity}, stack.module}
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
:ok
end
defp warn_unused_def({fun_arity, kind, meta, 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
:ok
end
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, 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
end
defp min_reachable_default(max, min, last, name, used) when max >= min do
fun_arity = {name, max}
case is_map_key(used, fun_arity) do
true -> min_reachable_default(max - 1, min, max, name, used)
false -> min_reachable_default(max - 1, min, last, name, used)
end
end
defp min_reachable_default(_max, _min, last, _name, _used) do
last
end
alias Module.Types.{Expr, Pattern}
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause}
false -> false
end
end
context = context()
handler = &local_handler(&1, &2, &3, &4, finder)
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
finder = fn _ -> {:dynamic, def} end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
try do
warnings_from_clause(meta, args, guards, body, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
context = warn_unused_clauses(defs, stack, context)
context.warnings
end
error =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
defp warn_unused_clauses(defs, stack, context) do
for {fun_arity, pending} <- context.local_used,
pending != [],
{_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0),
not Keyword.get(meta, :from_super, false),
reduce: context do
context ->
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
clauses_indexes =
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
The exception happened while checking this code:
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end
end
#{Macro.to_string(def_expr)}
defp local_handler(_meta, fun_arity, stack, context, finder) do
case context.local_sigs do
%{^fun_arity => {kind, inferred, _mapping}} ->
{kind, inferred, context}
Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")
%{^fun_arity => kind} when is_atom(kind) ->
{kind, :none, context}
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
{kind, inferred, context}
false ->
false
reraise error, __STACKTRACE__
end
end
end
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
expected = List.duplicate(Descr.dynamic(), arity)
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
{_, _, 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
{args_types, context} =
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{return_type, context} =
Expr.of_expr(body, stack, 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)
inferred = {:infer, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(context, clauses_context)}
end)
end
# 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.union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc])
defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]}
defp with_file_meta(stack, meta) do
defp with_file_meta(meta, file) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
:error -> stack
{:ok, {meta_file, _}} -> meta_file
:error -> file
end
end
defp internal_error!(e, trace, kind, meta, fun, args, guards, body, stack) do
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
exception =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(stack.module, fun, length(args))}:
#{Exception.format_banner(:error, e, trace)}\
The exception happened while checking this code:
#{Macro.to_string(def_expr)}
Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")
reraise exception, trace
end
defp guards_to_expr([], left) do
left
end
@@ -307,9 +51,17 @@ defmodule Module.Types do
guards_to_expr(guards, {:when, [], [left, guard]})
end
defp warnings_from_clause(meta, args, guards, body, stack, context) do
with {:ok, _types, context} <- Pattern.of_head(args, guards, meta, stack, context),
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
context.warnings
else
{:error, context} -> context.warnings
end
end
@doc false
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
when mode in @modes do
def stack(file, module, function, no_warn_undefined, cache) do
%{
# The fallback meta used for literals in patterns and guards
meta: [],
@@ -319,14 +71,12 @@ defmodule Module.Types do
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined or :all or Macro.Env indicating limited remotes
# List of calls to not warn on as undefined
no_warn_undefined: no_warn_undefined,
# A tuple with cache information (may be nil)
# A list of cached modules received from the parallel compiler
cache: cache,
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
# If variable refinements is enabled or not
refine: true
}
end
@@ -335,66 +85,8 @@ defmodule Module.Types do
%{
# A list of all warnings found so far
warnings: [],
# All vars and their types
vars: %{},
# 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: %{}
# Information about all vars and their types
vars: %{}
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
end
defp restore_context(%{vars: vars, failed: failed}, later_context) 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"
}
end
## Module errors
def format_error({:unused_args, {name, arity}}),
do: "default values for the optional arguments in #{name}/#{arity} are never used"
def format_error({:unused_args, {name, arity}, count}) when arity - count == 1,
do: "the default value for the last optional argument in #{name}/#{arity} is never used"
def format_error({:unused_args, {name, arity}, count}),
do:
"the default values for the last #{arity - count} optional arguments in #{name}/#{arity} are never used"
def format_error({:unused_def, {name, arity}, :defp}),
do: "function #{name}/#{arity} is unused"
def format_error({:unused_def, {name, arity}, :defmacrop}),
do: "macro #{name}/#{arity} is unused"
def format_error({:undefined_function, {f, a}, _})
when {f, a} in [__info__: 1, behaviour_info: 1, module_info: 1, module_info: 0],
do:
"undefined function #{f}/#{a} (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.#{f}/#{a})"
def format_error({:undefined_function, {f, a}, module}),
do:
"undefined function #{f}/#{a} (expected #{inspect(module)} to define such a function or for it to be imported, but none are available)"
def format_error({:incorrect_dispatch, {f, a}, _module}),
do: "cannot invoke macro #{f}/#{a} before its definition"
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+46 -187
View File
@@ -2,8 +2,6 @@ defmodule Module.Types.Helpers do
# AST and enumeration helpers.
@moduledoc false
## AST helpers
@doc """
Guard function to check if an AST node is a variable.
"""
@@ -16,37 +14,6 @@ defmodule Module.Types.Helpers do
end
end
@doc """
Unpacks a list into head elements and tail.
"""
def unpack_list([{:|, _, [head, tail]}], acc), do: {Enum.reverse([head | acc]), tail}
def unpack_list([head], acc), do: {Enum.reverse([head | acc]), []}
def unpack_list([head | tail], acc), do: unpack_list(tail, [head | acc])
@doc """
Unpacks a match into several matches.
"""
def unpack_match({:=, _, [left, right]}, acc),
do: unpack_match(left, unpack_match(right, acc))
def unpack_match(node, acc),
do: [node | acc]
@doc """
Returns the AST metadata.
"""
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Attaches span information.
"""
def with_span(meta, name) do
:elixir_env.calculate_span(meta, name)
end
## Warnings
@doc """
Formatted hints in typing errors.
"""
@@ -83,152 +50,36 @@ defmodule Module.Types.Helpers do
defp hint, do: :elixir_errors.prefix(:hint)
@doc """
Collect traces from variables in expression.
This information is exposed to language servers and
therefore must remain backwards compatible.
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
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}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
name: name,
context: context,
traces: collect_var_traces(off_traces)
})}
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
end)
versions
|> Map.values()
|> Enum.sort_by(& &1.name)
end
defp collect_var_traces(traces) do
traces
|> Enum.reject(fn {_expr, _file, type, _formatter} ->
# As an otimization do not care about dynamic terms
type == %{dynamic: :term}
end)
|> case do
[] -> traces
filtered -> filtered
end
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
{formatted_expr, formatter_hints} =
case formatter do
:default -> {expr_to_string(expr), []}
formatter -> formatter.(expr)
end
# This information is exposed to language servers and
# therefore must remain backwards compatible.
%{
file: file,
meta: meta,
formatted_expr: formatted_expr,
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true)
}
end)
|> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]})
|> Enum.dedup()
end
@doc """
Format previously collected traces.
"""
def format_traces(traces) do
Enum.map(traces, &format_trace/1)
end
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
traces =
for trace <- traces do
location =
trace.file
|> Path.relative_to_cwd()
|> Exception.format_file_line(trace.meta[:line], trace.meta[:column])
|> String.replace_suffix(":", "")
[
"""
# type: #{indent(trace.formatted_type, 4)}
# from: #{location}
\
""",
indent(trace.formatted_expr, 4),
?\n,
trace.formatted_hints
]
end
type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
end
@doc """
Formats a var for pretty printing.
"""
def format_var({var, _, context}), do: format_var(var, context)
def format_var(var, nil), do: "\"#{var}\""
def format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
defp expr_hints(_), do: []
@doc """
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
"""
def expr_to_string(expr) do
expr
|> Macro.prewalk(fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
|> reverse_rewrite()
|> Macro.to_string()
end
defp erl_to_ex(
:erlang,
:error,
[expr, :none, [error_info: {:%{}, _, [module: Exception]}]],
meta
) do
{:raise, meta, [expr]}
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args, _} -> {fun, meta, args}
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
@doc """
Returns the AST metadata.
"""
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Indents new lines.
"""
@@ -237,43 +88,51 @@ defmodule Module.Types.Helpers do
end
@doc """
Emits a warning.
Emits a warnings.
"""
def warn(module, warning, meta, stack, context) do
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, context)
end
end
defp effectively_warn(module, warning, meta, stack, context) do
{fun, arity} = stack.function
location = {stack.file, meta, {stack.module, fun, arity}}
%{context | warnings: [{module, warning, location} | context.warnings]}
end
@doc """
Emits an error.
In practice an error is a warning that halts other errors from being collected.
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
and stops on `{:error, reason}`.
"""
def error(module, warning, meta, stack, context) do
case context do
%{failed: true} ->
context
def reduce_ok(list, acc, fun) do
do_reduce_ok(list, acc, fun)
end
%{failed: false} ->
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, %{context | failed: true})
end
defp do_reduce_ok([head | tail], acc, fun) do
case fun.(head, acc) do
{:ok, acc} ->
do_reduce_ok(tail, acc, fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
@doc """
The type to return when there is an error.
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
and stops on `{:error, reason}`.
"""
def error_type, do: Module.Types.Descr.dynamic()
def map_reduce_ok(list, acc, fun) do
do_map_reduce_ok(list, {[], acc}, fun)
end
defp do_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elem, acc} ->
do_map_reduce_ok(tail, {[elem | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
end
+486 -236
View File
@@ -2,6 +2,8 @@ defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
alias Module.ParallelChecker
import Module.Types.{Helpers, Descr}
@prefix quote(do: ...)
@@ -24,23 +26,6 @@ defmodule Module.Types.Of do
type
end
@doc """
Marks a variable with error.
"""
def error_var(var, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
data = %{
type: error_type(),
name: var_name,
context: var_context,
off_traces: []
}
put_in(context.vars[version], data)
end
@doc """
Refines the type of a variable.
"""
@@ -62,8 +47,7 @@ defmodule Module.Types.Of do
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error, error_type(),
error({:refine_var, old_type, type, var, context}, meta, stack, context)}
{:error, warn({:refine_var, old_type, type, var, context}, meta, stack, context)}
else
{:ok, new_type, context}
end
@@ -95,94 +79,65 @@ defmodule Module.Types.Of do
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
{_optional?, value_type} ->
{value_type, context}
{:ok, value_type, context}
reason ->
{error_type(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
{:ok, dynamic(),
warn({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
end
end
@doc """
Builds a closed map.
"""
def closed_map(pairs, stack, context, of_fun) do
permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs ->
# TODO: Use the fallback type to actually indicate if open or closed.
if fallback == none(), do: closed_map(pairs), else: open_map(pairs)
end)
end
def closed_map(pairs, extra \\ [], stack, context, of_fun) do
result =
reduce_ok(pairs, {true, extra, [], context}, fn
{key, value}, {closed?, single, multiple, context} ->
with {:ok, keys, context} <- of_finite_key_type(key, stack, context, of_fun),
{:ok, value_type, context} <- of_fun.(value, stack, context) do
case keys do
:none ->
{:ok, {false, single, multiple, context}}
@doc """
Builds permutation of maps according to the given keys.
"""
def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do
context =
Enum.reduce(pairs, context, fn {key, value}, context ->
{_, context} = of_fun.(key, stack, context)
{_, context} = of_fun.(value, stack, context)
context
end)
[key] when multiple == [] ->
{:ok, {closed?, [{key, value_type} | single], multiple, context}}
{dynamic(), context}
end
def permutate_map(pairs, stack, context, of_fun, of_map) do
{dynamic?, fallback, single, multiple, assert, context} =
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
{key, value}, {dynamic?, fallback, single, multiple, assert, context} ->
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
{value_type, context} = of_fun.(value, stack, context)
dynamic? = dynamic? or dynamic_key? or gradual?(value_type)
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, context}
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert, context}
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context}
keys ->
{:ok, {closed?, single, [{keys, value_type} | multiple], context}}
end
end
end)
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
with {:ok, {closed?, single, multiple, context}} <- result do
map =
case Enum.reverse(multiple) do
[] ->
pairs = Enum.reverse(single)
if closed?, do: closed_map(pairs), else: open_map(pairs)
[{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)
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
pairs = Enum.reverse(single, [{key, type} | t])
if closed?, do: closed_map(pairs), else: open_map(pairs)
end
|> Enum.reduce(&union/2)
end
{:ok, map, context}
end
end
defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{:ok, [key], context}
end
defp of_finite_key_type(key, stack, context, of_fun) do
with {:ok, key_type, context} <- of_fun.(key, stack, context) do
case atom_fetch(key_type) do
{:finite, list} -> {:ok, list, context}
_ -> {:ok, :none, context}
end
if dynamic?, do: {dynamic(map), context}, else: {map, context}
end
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{false, [key], context}
end
defp finite_key_type(key, stack, context, of_fun) do
{key_type, context} = of_fun.(key, stack, context)
case atom_fetch(key_type) do
{:finite, list} -> {gradual?(key_type), list, context}
_ -> {gradual?(key_type), :none, context}
end
end
@@ -197,53 +152,50 @@ defmodule Module.Types.Of do
end
@doc """
Handles instantiation of a new struct.
Handles structs creation.
"""
def struct_instance(struct, args, meta, stack, context, of_fun)
def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun)
when is_atom(struct) do
# 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) ->
{type, context} = of_fun.(value, stack, context)
{{key, type}, context}
end)
{info, context} = struct_info(struct, meta, stack, context)
{struct_type(struct, info, args_types), context}
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
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, [], false, env) do
{:ok, info} -> {info, 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 =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
{info, context}
with {:ok, args_types, context} <-
map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) ->
with {:ok, type, context} <- of_fun.(value, stack, context) do
{:ok, {key, type}, context}
end
end) do
struct(struct, args_types, default_handling, meta, stack, context)
end
end
@doc """
Builds a type from the struct info.
Struct handling assuming the args have already been converted.
"""
def struct_type(struct, info, args_types \\ []) do
# TODO: Allow structs fields to be defined. If the fields are defined,
# then the struct is no longer dynamic. And we need to validate args
# against the struct types.
# TODO: Use the struct default values to define the default types.
def struct(struct, args_types, default_handling, meta, stack, context) do
context = remote(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
term = term()
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)
defaults =
for %{field: field} <- info, field != :__struct__ do
{field, term}
end
pairs =
case default_handling do
:merge_defaults -> [{:__struct__, atom([struct])} | defaults] ++ args_types
:skip_defaults -> [{:__struct__, atom([struct])} | args_types]
:only_defaults -> [{:__struct__, atom([struct])} | defaults]
end
{:ok, dynamic(closed_map(pairs)), context}
end
## Binary
@@ -254,122 +206,211 @@ defmodule Module.Types.Of do
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _kind, _stack, context) do
context
def binary([], _kind, _stack, context, _of_fun) do
{:ok, context}
end
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
def binary([head], kind, stack, context, of_fun) do
binary_segment(head, kind, [head], stack, context, of_fun)
end
def binary([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
def binary([head | tail], kind, stack, context, of_fun) do
case binary_segment(head, kind, [head, @suffix], stack, context, of_fun) do
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_many([last], kind, stack, context) do
binary_segment(last, kind, [@prefix, last], stack, context)
defp binary_many([last], kind, stack, context, of_fun) do
binary_segment(last, kind, [@prefix, last], stack, context, of_fun)
end
defp binary_many([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
defp binary_many([head | tail], kind, stack, context, of_fun) do
case binary_segment(head, kind, [@prefix, head, @suffix], stack, context, of_fun) do
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
# If the segment is a literal, the compiler has already checked its validity,
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context, _of_fun)
when is_binary(left) or is_number(left) do
context
{:ok, context}
end
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context, of_fun) do
expected_type = specifier_info(kind, right)
expr = {:<<>>, meta, args}
{_type, context} =
case kind do
:match ->
Module.Types.Pattern.of_match_var(left, type, expr, stack, context)
:guard ->
Module.Types.Pattern.of_guard(left, type, expr, stack, context)
:expr ->
{actual, context} = Module.Types.Expr.of_expr(left, stack, context)
intersect(actual, type, expr, stack, context)
end
specifier_size(kind, right, stack, context)
with {:ok, _type, context} <- of_fun.(left, {expected_type, expr}, stack, context) do
{:ok, context}
end
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
defp specifier_info(kind, {:-, _, [left, _right]}), do: specifier_info(kind, left)
defp specifier_info(:expr, {:float, _, _}), do: @integer_or_float
defp specifier_info(:expr, {:utf8, _, _}), do: @integer_or_binary
defp specifier_info(:expr, {:utf16, _, _}), do: @integer_or_binary
defp specifier_info(:expr, {:utf32, _, _}), do: @integer_or_binary
defp specifier_info(:pattern, {:utf8, _, _}), do: @integer
defp specifier_info(:pattern, {:utf16, _, _}), do: @integer
defp specifier_info(:pattern, {:utf32, _, _}), do: @integer
defp specifier_info(:pattern, {:float, _, _}), do: @float
defp specifier_info(_kind, {:integer, _, _}), do: @integer
defp specifier_info(_kind, {:bits, _, _}), do: @binary
defp specifier_info(_kind, {:bitstring, _, _}), do: @binary
defp specifier_info(_kind, {:bytes, _, _}), do: @binary
defp specifier_info(_kind, {:binary, _, _}), do: @binary
defp specifier_info(_kind, _specifier), do: @integer
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
## Apply
def apply(:erlang, name, [left, right], expr, stack, context)
when name in [:>=, :"=<", :>, :<, :min, :max] do
result = if name in [:min, :max], do: union(left, right), else: boolean()
cond do
match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) ->
warning = {:struct_comparison, expr, context}
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
number_type?(left) and number_type?(right) ->
{:ok, result, context}
empty?(intersection(left, right)) ->
warning = {:mismatched_comparison, expr, context}
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
true ->
{:ok, result, context}
end
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{actual, context} = Module.Types.Expr.of_expr(arg, stack, context)
{_, context} = intersect(actual, integer(), expr, stack, context)
context
def apply(mod, name, args, expr, stack, context) do
case :elixir_rewrite.inline(mod, name, length(args)) do
{mod, name} -> apply(mod, name, args, expr, stack, context)
false -> {:ok, dynamic(), context}
end
end
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{_type, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
context
end
## Remote
defp specifier_size(_kind, _specifier, _stack, context) do
context
end
## Modules
@doc """
Returns modules in a type.
The call information is used on report reporting.
"""
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
def remote(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{_, mods} ->
context =
Enum.reduce(mods, context, fn mod, context ->
remote(mod, fun, arity, meta, stack, context)
end)
{mods, context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)}
{[], warn(warning, meta, stack, context)}
end
end
@doc """
Checks a module is a valid remote.
"""
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
if Keyword.get(meta, :runtime_module, false) do
context
else
ParallelChecker.preload_module(stack.cache, module)
check_export(module, fun, arity, meta, stack, context)
end
end
defp check_export(module, fun, arity, meta, stack, context) do
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
{:ok, mode, :def, reason} ->
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
{:ok, mode, :defmacro, reason} ->
context = warn({:unrequired_module, module, fun, arity}, meta, stack, context)
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, stack) do
warn({:undefined_module, module, fun, arity}, meta, stack, context)
else
context
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, stack) do
exports = ParallelChecker.all_exports(stack.cache, module)
warn({:undefined_function, module, fun, arity, exports}, meta, stack, context)
else
context
end
end
end
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
if reason do
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> :string.titlecase()
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
reason = string |> List.to_string() |> :string.titlecase()
reason = "It will be removed in #{removal}. #{reason}"
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
_ ->
context
end
end
# The protocol code dispatches to unknown modules, so we ignore them here.
#
# try do
# SomeProtocol.Atom.__impl__
# rescue
# ...
# end
#
# But for protocols we don't want to traverse the protocol code anyway.
# TODO: remove this clause once we no longer traverse the protocol code.
defp warn_undefined?(_module, :__impl__, 1, _stack), do: false
defp warn_undefined?(_module, :module_info, 0, _stack), do: false
defp warn_undefined?(_module, :module_info, 1, _stack), do: false
defp warn_undefined?(:erlang, :orelse, 2, _stack), do: false
defp warn_undefined?(:erlang, :andalso, 2, _stack), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, stack) do
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
## Warning helpers
@doc """
Intersects two types and emit an incompatible error if empty.
Intersects two types and emit an incompatible warning if empty.
"""
def intersect(actual, expected, expr, stack, context) do
def intersect(actual, {expected, expr}, stack, context) do
type = intersection(actual, expected)
if empty?(type) do
{error_type(), incompatible_error(expr, expected, actual, stack, context)}
{:error, incompatible_warn(expr, expected, actual, stack, context)}
else
{type, context}
{:ok, type, context}
end
end
@@ -379,17 +420,112 @@ defmodule Module.Types.Of do
This is a generic warning for when the expected/actual types
themselves may come from several different circumstances.
"""
def incompatible_error(expr, expected_type, actual_type, stack, context) do
def incompatible_warn(expr, expected_type, actual_type, stack, context) do
meta = get_meta(expr) || stack.meta
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
warning = {:incompatible, expr, expected_type, actual_type, hints, context}
error(warning, meta, stack, context)
warn(warning, meta, stack, context)
end
defp error(warning, meta, stack, context) do
error(__MODULE__, warning, meta, stack, context)
defp warn(warning, meta, stack, context) do
warn(__MODULE__, warning, meta, stack, context)
end
## Traces
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
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}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
name: name,
context: context,
traces: collect_var_traces(off_traces)
})}
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
end)
versions
|> Map.values()
|> Enum.sort_by(& &1.name)
end
defp collect_var_traces(traces) do
traces
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
{formatted_expr, formatter_hints} =
case formatter do
:default -> {expr_to_string(expr), []}
formatter -> formatter.(expr)
end
%{
file: file,
meta: meta,
formatted_expr: formatted_expr,
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: to_quoted_string(type)
}
end)
end
def format_traces(traces) do
Enum.map(traces, &format_trace/1)
end
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
traces =
for trace <- traces do
location =
trace.file
|> Path.relative_to_cwd()
|> Exception.format_file_line(trace.meta[:line])
|> String.replace_suffix(":", "")
[
"""
# type: #{indent(trace.formatted_type, 4)}
# from: #{location}
\
""",
indent(trace.formatted_expr, 4),
?\n,
trace.formatted_hints
]
end
type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
end
defp format_var({var, _, context}), do: format_var(var, context)
defp format_var(var, nil), do: "\"#{var}\""
defp format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
defp expr_hints(_), do: []
## Warning formatting
def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
@@ -421,38 +557,14 @@ defmodule Module.Types.Of do
#{expr_to_string(expr) |> indent(4)}
got type:
#{to_quoted_string(actual_type) |> indent(4)}
but expected type:
expected type:
#{to_quoted_string(expected_type) |> indent(4)}
""",
format_traces(traces),
format_hints(hints)
])
}
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
#{to_quoted_string(actual_type) |> indent(4)}
""",
format_traces(traces),
format_hints(hints)
])
@@ -495,16 +607,154 @@ defmodule Module.Types.Of do
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)}
""",
"""),
format_traces(traces)
])
}
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
end
def format_diagnostic({:mismatched_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison between incompatible types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
While Elixir can compare across all types, you are comparing \
across types which are always distinct, and the result is either \
always true or always false
"""
])
}
end
def format_diagnostic({:struct_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
end
def format_diagnostic({:undefined_module, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
else
Exception.format_mfa(module, fun, arity)
end
%{
message:
IO.iodata_to_binary([
top,
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]),
group: true
}
end
def format_diagnostic({:undefined_function, module, :__struct__, 0, _exports}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
group: true
}
end
def format_diagnostic({:undefined_function, module, fun, arity, exports}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]),
group: true
}
end
def format_diagnostic({:deprecated, module, fun, arity, reason}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]),
group: true
}
end
def format_diagnostic({:unrequired_module, module, fun, arity}) do
%{
message:
IO.iodata_to_binary([
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]),
group: true
}
end
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -865,13 +865,7 @@ defmodule OptionParser do
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
end
"#{option} : Missing argument of type #{type}"
else
msg = "#{option} : Unknown option"
+49 -111
View File
@@ -96,7 +96,7 @@ defmodule PartitionSupervisor do
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end})
You can start the dynamic supervisors under a `PartitionSupervisor`:
You can do start the dynamic supervisors under a `PartitionSupervisor`:
children = [
{PartitionSupervisor,
@@ -151,7 +151,7 @@ defmodule PartitionSupervisor do
@doc false
def child_spec(opts) when is_list(opts) do
id =
case Keyword.get(opts, :name, PartitionSupervisor) do
case Keyword.get(opts, :name, DynamicSupervisor) do
name when is_atom(name) -> name
{:via, _module, name} -> name
end
@@ -228,26 +228,26 @@ defmodule PartitionSupervisor do
def start_link(opts) when is_list(opts) do
name = opts[:name]
if !name do
unless name do
raise ArgumentError, "the :name option must be given to PartitionSupervisor"
end
{child_spec, opts} = Keyword.pop(opts, :child_spec)
if !child_spec do
unless child_spec do
raise ArgumentError, "the :child_spec option must be given to PartitionSupervisor"
end
{partitions, opts} = Keyword.pop(opts, :partitions, System.schedulers_online())
if not (is_integer(partitions) and partitions >= 1) do
unless is_integer(partitions) and partitions >= 1 do
raise ArgumentError,
"the :partitions option must be a positive integer, got: #{inspect(partitions)}"
end
{with_arguments, opts} = Keyword.pop(opts, :with_arguments, fn args, _partition -> args end)
if not is_function(with_arguments, 2) do
unless is_function(with_arguments, 2) do
raise ArgumentError,
"the :with_arguments option must be a function that receives two arguments, " <>
"the current call arguments and the partition, got: #{inspect(with_arguments)}"
@@ -260,17 +260,17 @@ defmodule PartitionSupervisor do
for partition <- 0..(partitions - 1) do
args = with_arguments.(args, partition)
if not is_list(args) do
unless is_list(args) do
raise "the call to the function in :with_arguments must return a list, got: #{inspect(args)}"
end
start = {__MODULE__, :start_child, [mod, fun, args, partition]}
start = {__MODULE__, :start_child, [mod, fun, args, name, partition]}
Map.merge(map, %{id: partition, start: start, modules: modules})
end
auto_shutdown = Keyword.get(opts, :auto_shutdown, :never)
if auto_shutdown != :never do
unless auto_shutdown == :never do
raise ArgumentError,
"the :auto_shutdown option must be :never, got: #{inspect(auto_shutdown)}"
end
@@ -282,14 +282,14 @@ defmodule PartitionSupervisor do
end
@doc false
def start_child(mod, fun, args, partition) do
def start_child(mod, fun, args, name, partition) do
case apply(mod, fun, args) do
{:ok, pid} ->
register_child(partition, pid)
register_child(name, partition, pid)
{:ok, pid}
{:ok, pid, info} ->
register_child(partition, pid)
register_child(name, partition, pid)
{:ok, pid, info}
other ->
@@ -297,97 +297,33 @@ defmodule PartitionSupervisor do
end
end
defp register_child(partition, pid) do
:ets.insert(Process.get(:ets_table), {partition, pid})
defp register_child(name, partition, pid) when is_atom(name) do
:ets.insert(name, {partition, pid})
end
defp register_child({:via, _, _}, partition, pid) do
Registry.register(@registry, {self(), partition}, pid)
end
@impl true
def init({name, partitions, children, init_opts}) do
table = init_table(name)
:ets.insert(table, {:partitions, partitions, partitions})
Process.put(:ets_table, table)
init_partitions(name, partitions)
Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one))
end
defp init_table(name) when is_atom(name) do
:ets.new(name, [:set, :named_table, :public, read_concurrency: true])
defp init_partitions(name, partitions) when is_atom(name) do
:ets.new(name, [:set, :named_table, :protected, read_concurrency: true])
:ets.insert(name, {:partitions, partitions})
end
defp init_table({:via, _, _}) do
table = :ets.new(__MODULE__, [:set, :public, read_concurrency: true])
ensure_registry()
Registry.register(@registry, self(), table)
table
end
defp init_partitions({:via, _, _}, partitions) do
child_spec = {Registry, keys: :unique, name: @registry}
defp ensure_registry do
if Process.whereis(@registry) == nil do
Supervisor.start_child(:elixir_sup, {Registry, keys: :unique, name: @registry})
unless Process.whereis(@registry) do
Supervisor.start_child(:elixir_sup, child_spec)
end
end
@doc """
Resizes the number of partitions in the PartitionSupervisor.
This is done by starting or stopping a given number of
partitions in the supervisor. All of the child specifications
are kept in the `PartitionSupervisor` itself.
The final number of partitions cannot be less than zero and
cannot be more than the number of partitions the supervisor
started with.
"""
@doc since: "1.18.0"
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
def resize!(name, partitions) when is_integer(partitions) do
supervisor =
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
table = table(name)
ensure_registry()
Registry.lock(@registry, supervisor, fn ->
case :ets.lookup(table, :partitions) do
[{:partitions, _current, max}] when partitions not in 0..max//1 ->
raise ArgumentError,
"the number of partitions to resize to must be a number between 0 and #{max}, got: #{partitions}"
[{:partitions, current, max}] when partitions > current ->
for id <- current..(partitions - 1) do
case Supervisor.restart_child(supervisor, id) do
{:ok, _} ->
:ok
{:ok, _, _} ->
:ok
{:error, reason} ->
raise "cannot restart partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
end
end
:ets.insert(table, {:partitions, partitions, max})
current
[{:partitions, current, max}] when partitions < current ->
:ets.insert(table, {:partitions, partitions, max})
for id <- partitions..(current - 1) do
case Supervisor.terminate_child(supervisor, id) do
:ok ->
:ok
{:error, reason} ->
raise "cannot terminate partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
end
end
current
[{:partitions, current, _max}] ->
current
end
end)
Registry.register(@registry, self(), partitions)
end
@doc """
@@ -396,27 +332,24 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec partitions(name()) :: pos_integer()
def partitions(name) do
name |> table() |> partitions(name)
{_name, partitions} = name_partitions(name)
partitions
end
defp partitions(table, name) do
# For whereis_name, we want to lookup on GenServer.whereis/1
# just once, so we lookup the name and partitions together.
defp name_partitions(name) when is_atom(name) do
try do
:ets.lookup_element(table, :partitions, 2)
{name, :ets.lookup_element(name, :partitions, 2)}
rescue
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
end
defp table(name) when is_atom(name) do
name
end
# For whereis_name, we want to lookup on GenServer.whereis/1
# just once, so we lookup the name and partitions together.
defp table(name) when is_tuple(name) do
defp name_partitions(name) when is_tuple(name) do
with pid when is_pid(pid) <- GenServer.whereis(name),
[{_, table}] <- Registry.lookup(@registry, pid) do
table
[name_partitions] <- Registry.lookup(@registry, pid) do
name_partitions
else
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
@@ -441,7 +374,7 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec which_children(name()) :: [
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
Supervisor.which_children(name)
@@ -495,17 +428,22 @@ defmodule PartitionSupervisor do
@doc false
def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do
table = table(name)
partitions = partitions(table, name)
if partitions == 0 do
raise ArgumentError, "PartitionSupervisor #{inspect(name)} has zero partitions"
end
{name, partitions} = name_partitions(name)
partition =
if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions)
:ets.lookup_element(table, partition, 2)
whereis_name(name, partition)
end
defp whereis_name(name, partition) when is_atom(name) do
:ets.lookup_element(name, partition, 2)
end
defp whereis_name(name, partition) when is_pid(name) do
@registry
|> Registry.values({name, partition}, name)
|> List.first(:undefined)
end
@doc false
+3 -3
View File
@@ -884,7 +884,7 @@ defmodule Path do
* A `..` component would make it so that the path would traverse up above
the root of `relative_to`.
* A symbolic link in the path points to something above the root of `relative_to`.
* A symbolic link in the path points to something above the root of `cwd`.
## Examples
@@ -906,10 +906,10 @@ defmodule Path do
"""
@doc since: "1.14.0"
@spec safe_relative(t, t) :: {:ok, binary} | :error
def safe_relative(path, relative_to \\ File.cwd!()) do
def safe_relative(path, cwd \\ File.cwd!()) do
path = IO.chardata_to_string(path)
case :filelib.safe_relative_path(path, relative_to) do
case :filelib.safe_relative_path(path, cwd) do
:unsafe -> :error
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
end
+2 -34
View File
@@ -88,9 +88,6 @@ defmodule Process do
`false`.
`pid` must refer to a process running on the local node or `ArgumentError` is raised.
To check whether a process on any node is alive you can use the [`:erpc`](`:erpc`) module.
:erpc.call(node(pid), Process, :alive?, [pid])
Inlined by the compiler.
"""
@@ -219,16 +216,6 @@ defmodule Process do
Inlined by the compiler.
> #### Differences to `Kernel.exit/1` {: .info }
>
> The functions `Kernel.exit/1` and `Process.exit/2` are
> named similarly but provide very different functionalities. The
> `Kernel:exit/1` function should be used when the intent is to stop the current
> process while `Process.exit/2` should be used when the intent is to send an
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
> when the signal is different than `:kill`.
## Examples
Process.exit(pid, :kill)
@@ -246,12 +233,6 @@ defmodule Process do
the current process will sleep forever, and not
consume or reply to messages.
> #### Sleeping limit {: .info }
>
> Before Elixir v1.18, `sleep/1` did not accept integer timeout values greater
> than `16#ffffffff`, that is, `2^32-1`. Since Elixir v1.18, arbitrarily-high integer
> values are accepted.
**Use this function with extreme care**. For almost all situations
where you would use `sleep/1` in Elixir, there is likely a
more correct, faster and precise way of achieving the same with
@@ -318,15 +299,7 @@ defmodule Process do
end
"""
# Max value for a receive's after clause
@max_receive_after 0xFFFFFFFF
@spec sleep(timeout) :: :ok
def sleep(timeout) when is_integer(timeout) and timeout > @max_receive_after do
receive after: (@max_receive_after -> sleep(timeout - @max_receive_after))
end
def sleep(timeout)
when is_integer(timeout) and timeout >= 0
when timeout == :infinity do
@@ -579,15 +552,10 @@ defmodule Process do
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
receive do: msg -> msg
#=> :pong
ref_and_alias = Process.monitor(pid, alias: :reply_demonitor)
#=> #Reference<0.906660723.3006791681.40191>
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
receive do: msg -> msg
#=> {:DOWN, #Reference<0.906660723.3006791681.40191>, :process, #PID<0.118.0>, :noproc}
"""
+130 -136
View File
@@ -6,7 +6,7 @@ defmodule Protocol do
implementations. A protocol is defined with `Kernel.defprotocol/2`
and its implementations with `Kernel.defimpl/3`.
## Example
## A real case
In Elixir, we have two nouns for checking how many items there
are in a data structure: `length` and `size`. `length` means the
@@ -102,7 +102,8 @@ defmodule Protocol do
invoking the protocol will raise unless it is configured to
fall back to `Any`. Conveniences for building implementations
on top of existing ones are also available, look at `defstruct/1`
for more information about deriving protocols.
for more information about deriving
protocols.
## Fallback to `Any`
@@ -164,16 +165,56 @@ defmodule Protocol do
The `@spec` above expresses that all types allowed to implement the
given protocol are valid argument types for the given function.
## Configuration
## Reflection
The following module attributes are available to configure a protocol:
Any protocol module contains three extra functions:
* `@fallback_to_any` - when true, enables protocol dispatch to
fallback to any
* `__protocol__/1` - returns the protocol information. The function takes
one of the following atoms:
* `@undefined_impl_description` - a string with additional description
to be used on `Protocol.UndefinedError` when looking up the implementation
fails. This option is only applied if `@fallback_to_any` is not set to true
* `:consolidated?` - returns whether the protocol is consolidated
* `:functions` - returns a keyword list of protocol functions and their arities
* `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
implementing the protocol, otherwise `:not_consolidated`
* `:module` - the protocol module atom name
* `impl_for/1` - returns the module that implements the protocol for the given argument,
`nil` otherwise
* `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is
not found
For example, for the `Enumerable` protocol we have:
iex> Enumerable.__protocol__(:functions)
[count: 1, member?: 2, reduce: 3, slice: 1]
iex> Enumerable.impl_for([])
Enumerable.List
iex> Enumerable.impl_for(42)
nil
In addition, every protocol implementation module contains the `__impl__/1`
function. The function takes one of the following atoms:
* `:for` - returns the module responsible for the data structure of the
protocol implementation
* `:protocol` - returns the protocol module for which this implementation
is provided
For example, the module implementing the `Enumerable` protocol for lists is
`Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:
iex(1)> Enumerable.List.__impl__(:for)
List
iex(2)> Enumerable.List.__impl__(:protocol)
Enumerable
## Consolidation
@@ -181,8 +222,7 @@ defmodule Protocol do
are known up-front, typically after all Elixir code in a project is compiled,
Elixir provides a feature called *protocol consolidation*. Consolidation directly
links protocols to their implementations in a way that invoking a function from a
consolidated protocol is equivalent to invoking two remote functions - one to
identify the correct implementation, and another to call the implementation.
consolidated protocol is equivalent to invoking two remote functions.
Protocol consolidation is applied by default to all Mix projects during compilation.
This may be an issue during test. For instance, if you want to implement a protocol
@@ -228,41 +268,6 @@ defmodule Protocol do
globally set.
"""
@doc """
A function available in all protocol definitions that returns protocol metadata.
"""
@callback __protocol__(:consolidated?) :: boolean()
@callback __protocol__(:functions) :: [{atom(), arity()}]
@callback __protocol__(:impls) :: {:consolidated, [module()]} | :not_consolidated
@callback __protocol__(:module) :: module()
@doc """
A function available in all protocol definitions that returns the implementation
for the given `term` or nil.
If `@fallback_to_any` is true, `nil` is never returned.
"""
@callback impl_for(term) :: module() | nil
@doc """
A function available in all protocol definitions that returns the implementation
for the given `term` or raises.
If `@fallback_to_any` is true, it never raises.
"""
@callback impl_for!(term) :: module()
@doc """
An optional callback to be implemented by protocol authors for custom deriving.
It must return a quoted expression that implements the protocol for the given module.
See `Protocol.derive/3` for an example.
"""
@macrocallback __deriving__(module(), term()) :: Macro.t()
@optional_callbacks __deriving__: 2
@doc false
defmacro def(signature)
@@ -347,7 +352,7 @@ defmodule Protocol do
end
defp assert_impl!(protocol, base, extra) do
impl = Protocol.__concat__(protocol, base)
impl = Module.concat(protocol, base)
try do
Code.ensure_compiled!(impl)
@@ -375,50 +380,52 @@ defmodule Protocol do
@doc """
Derives the `protocol` for `module` with the given options.
Every time you derive a protocol, Elixir will verify if the protocol
has implemented the `c:Protocol.__deriving__/2` callback. If so,
the callback will be invoked and it should define the implementation
module. Otherwise an implementation that simply points to the `Any`
implementation is automatically derived.
If your implementation passes options or if you are generating
custom code based on the struct, you will also need to implement
a macro defined as `__deriving__(module, struct, options)`
to get the options that were passed.
## Examples
defprotocol Derivable do
@impl true
defmacro __deriving__(module, options) do
# If you need to load struct metadata, you may call:
# struct_info = Macro.struct_info!(module, __CALLER__)
def ok(arg)
end
defimpl Derivable, for: Any do
defmacro __deriving__(module, struct, options) do
quote do
defimpl Derivable, for: unquote(module) do
def ok(arg) do
{:ok, arg, unquote(options)}
{:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
end
end
end
end
def ok(arg)
def ok(arg) do
{:ok, arg}
end
end
Once the protocol is defined, there are two ways it can be
derived. The first is by using the `@derive` module attribute
by the time you define the struct:
defmodule ImplStruct do
@derive [Derivable]
defstruct a: 0, b: 0
end
Derivable.ok(%ImplStruct{})
#=> {:ok, %ImplStruct{a: 0, b: 0}, []}
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
If the struct has already been defined, you can call this macro:
Explicit derivations can now be called via `__deriving__/3`:
# Explicitly derived via `__deriving__/3`
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}
# Explicitly derived by API via `__deriving__/3`
require Protocol
Protocol.derive(Derivable, ImplStruct, :oops)
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, :oops}
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}
"""
defmacro derive(protocol, module, options \\ []) do
@@ -490,8 +497,7 @@ defmodule Protocol do
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
# Do not use protocols as they may be consolidating
path = if(is_list(path), do: path, else: String.to_charlist(path)),
path = to_charlist(path),
file <- list_dir(path),
mod = extract_from_file(path, file, prefix, callback),
do: mod
@@ -678,7 +684,7 @@ defmodule Protocol do
end
defp load_impl(protocol, for) do
Protocol.__concat__(protocol, for)
Module.concat(protocol, for)
end
# Finally compile the module and emit its bytecode.
@@ -693,14 +699,25 @@ defmodule Protocol do
def __protocol__(name, do: block) do
quote do
defmodule unquote(name) do
@behaviour Protocol
@before_compile Protocol
# We don't allow function definition inside protocols
import Kernel,
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
except: [
def: 1,
def: 2,
defp: 1,
defp: 2,
defdelegate: 2,
defguard: 1,
defguardp: 1,
defmacro: 1,
defmacro: 2,
defmacrop: 1,
defmacrop: 2
]
# Import the new `def` that is used by protocols
# Import the new dsl that holds the new def
import Protocol, only: [def: 1]
# Compile with debug info for consolidation
@@ -719,17 +736,19 @@ defmodule Protocol do
end
end
defp callback_ast_to_fa({_kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) do
defp callback_ast_to_fa({kind, {:"::", meta, [{name, _, args}, _return]}, _pos})
when kind in [:callback, :macrocallback] do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa(
{_kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
) do
{kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
)
when kind in [:callback, :macrocallback] do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa({_kind, _clause, _pos}) do
defp callback_ast_to_fa({kind, _, _pos}) when kind in [:callback, :macrocallback] do
[]
end
@@ -747,10 +766,13 @@ defmodule Protocol do
end
defp warn(message, env, line) when is_integer(line) do
IO.warn(message, %{env | line: line})
stacktrace = :maps.update(:line, line, env) |> Macro.Env.stacktrace()
IO.warn(message, stacktrace)
end
def __before_compile__(env) do
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
functions = Module.get_attribute(env.module, :__functions__)
if functions == [] do
@@ -761,11 +783,8 @@ defmodule Protocol do
)
end
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
# TODO: Convert the following warnings into errors in future Elixir versions
:lists.foreach(
:lists.map(
fn {name, arity} = fa ->
warn(
"cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
@@ -780,7 +799,7 @@ defmodule Protocol do
macrocallback_metas = callback_metas(env.module, :macrocallback)
macrocallbacks = :maps.keys(macrocallback_metas)
:lists.foreach(
:lists.map(
fn {name, arity} = fa ->
warn(
"cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
@@ -807,7 +826,7 @@ defmodule Protocol do
quote bind_quoted: [built_in: __built_in__()] do
any_impl_for =
if @fallback_to_any do
__MODULE__.Any
quote do: unquote(__MODULE__.Any)
else
nil
end
@@ -831,7 +850,7 @@ defmodule Protocol do
# Define the implementation for built-ins
:lists.foreach(
fn {guard, mod} ->
target = Protocol.__concat__(__MODULE__, mod)
target = Module.concat(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
@@ -853,9 +872,6 @@ defmodule Protocol do
unquote(any_impl_for)
end
undefined_impl_description =
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
@doc false
@spec impl_for!(term) :: atom
if any_impl_for do
@@ -864,27 +880,23 @@ defmodule Protocol do
end
else
Kernel.def impl_for!(data) do
impl_for(data) ||
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: unquote(undefined_impl_description)
)
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
end
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
target =
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
unless Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@@ -898,9 +910,9 @@ defmodule Protocol do
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))
@spec __protocol__(:consolidated?) :: boolean
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
@@ -908,6 +920,12 @@ defmodule Protocol do
end
end
@doc false
def __functions_spec__([]), do: []
def __functions_spec__([head | tail]),
do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]
@doc false
def __impl__(protocol, opts, do_block, env) do
opts = Keyword.merge(opts, do_block)
@@ -948,7 +966,7 @@ defmodule Protocol do
quote do
protocol = unquote(protocol)
for = unquote(for)
name = Protocol.__concat__(protocol, for)
name = Module.concat(protocol, for)
Protocol.assert_protocol!(protocol)
Protocol.__ensure_defimpl__(protocol, for, __ENV__)
@@ -971,12 +989,14 @@ defmodule Protocol do
@doc false
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
struct = Macro.struct!(for, env)
foreach = fn
proto when is_atom(proto) ->
derive(proto, for, [], env)
derive(proto, for, struct, [], env)
{proto, opts} when is_atom(proto) ->
derive(proto, for, opts, env)
derive(proto, for, struct, opts, env)
end
:lists.foreach(foreach, :lists.flatten(derives))
@@ -984,30 +1004,21 @@ defmodule Protocol do
:ok
end
defp derive(protocol, for, opts, env) do
defp derive(protocol, for, struct, opts, env) do
extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
assert_protocol!(protocol, extra)
{mod, args} =
if macro_exported?(protocol, :__deriving__, 2) do
{protocol, [for, opts]}
else
# TODO: Deprecate this on Elixir v1.22+
assert_impl!(protocol, Any, extra)
{Protocol.__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]}
end
__ensure_defimpl__(protocol, for, env)
assert_impl!(protocol, Any, extra)
# Clean up variables from eval context
env = :elixir_env.reset_vars(env)
args = [for, struct, opts]
impl = Module.concat(protocol, Any)
:elixir_module.expand_callback(env.line, mod, :__deriving__, args, env, fn mod, fun, args ->
:elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args ->
if function_exported?(mod, fun, length(args)) do
apply(mod, fun, args)
else
__ensure_defimpl__(protocol, for, env)
assert_impl!(protocol, Any, extra)
impl = Protocol.__concat__(protocol, Any)
funs =
for {fun, arity} <- protocol.__protocol__(:functions) do
args = Macro.generate_arguments(arity, nil)
@@ -1031,11 +1042,7 @@ defmodule Protocol do
def __impl__(:for), do: unquote(for)
end
Module.create(
Protocol.__concat__(protocol, for),
[quoted | funs],
Macro.Env.location(env)
)
Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env))
end
end)
end
@@ -1074,17 +1081,4 @@ defmodule Protocol do
is_reference: Reference
]
end
@doc false
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
defp ensure_prefix(left), do: "Elixir." <> left
defp remove_prefix("Elixir." <> right), do: right
defp remove_prefix(right), do: right
end
+2 -16
View File
@@ -184,22 +184,8 @@ defmodule Range do
@spec new(limit, limit) :: t
def new(first, last) when is_integer(first) and is_integer(last) do
step =
if first <= last do
1
else
# TODO: Remove me on v2.0
IO.warn_once(
{__MODULE__, :new},
fn ->
"Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention"
end,
3
)
-1
end
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.18
step = if first <= last, do: 1, else: -1
%Range{first: first, last: last, step: step}
end
+4 -6
View File
@@ -36,9 +36,10 @@ defmodule Record do
## Reflection
The record tag and its fields are stored as metadata in the "Docs" chunk
of the record definition macro. You can retrieve the documentation for
a module by calling `Code.fetch_docs/1`.
A list of all records in a module, if any, can be retrieved by reading the
`@__records__` module attribute. It returns a list of maps with the record
kind, name, tag, and fields. The attribute is only available inside the
module definition.
"""
@doc """
@@ -252,7 +253,6 @@ defmodule Record do
tag = tag || name
fields = Record.__record__(__MODULE__, :defrecord, name, tag, kv)
@doc record: {tag, fields}
defmacro unquote(name)(args \\ []) do
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
end
@@ -312,8 +312,6 @@ defmodule Record do
error_on_duplicate_record(module, name)
fields = fields(kind, kv)
# TODO: Remove me on Elixir v2.0
Module.register_attribute(module, :__records__, accumulate: true)
Module.put_attribute(module, :__records__, %{
+19 -16
View File
@@ -209,17 +209,20 @@ defmodule Regex do
## Examples
Regex.compile("foo")
#=> {:ok, ~r/foo/}
iex> Regex.compile("foo")
{:ok, ~r/foo/}
Regex.compile("foo", "i")
#=>{:ok, ~r/foo/i}
iex> Regex.compile("*foo")
{:error, {~c"nothing to repeat", 0}}
Regex.compile("*foo")
#=> {:error, {~c"nothing to repeat", 0}}
iex> Regex.compile("foo", "i")
{:ok, ~r/foo/i}
iex> Regex.compile("foo", [:caseless])
{:ok, Regex.compile!("foo", [:caseless])}
"""
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, term}
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
def compile(source, opts \\ "") when is_binary(source) do
compile(source, opts, version())
end
@@ -564,31 +567,31 @@ defmodule Regex do
## Examples
iex> Regex.split(~r/-/, "a-b-c")
iex> Regex.split(~r{-}, "a-b-c")
["a", "b", "c"]
iex> Regex.split(~r/-/, "a-b-c", parts: 2)
iex> Regex.split(~r{-}, "a-b-c", parts: 2)
["a", "b-c"]
iex> Regex.split(~r/-/, "abc")
iex> Regex.split(~r{-}, "abc")
["abc"]
iex> Regex.split(~r//, "abc")
iex> Regex.split(~r{}, "abc")
["", "a", "b", "c", ""]
iex> Regex.split(~r/a(?<second>b)c/, "abc")
iex> Regex.split(~r{a(?<second>b)c}, "abc")
["", ""]
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second])
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second])
["a", "c"]
iex> Regex.split(~r/(x)/, "Elixir", include_captures: true)
iex> Regex.split(~r{(x)}, "Elixir", include_captures: true)
["Eli", "x", "ir"]
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second], include_captures: true)
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second], include_captures: true)
["a", "b", "c"]
iex> Regex.split(~r/-/, "-a-b--c", trim: true)
iex> Regex.split(~r{-}, "-a-b--c", trim: true)
["a", "b", "c"]
"""
+26 -150
View File
@@ -331,7 +331,7 @@ defmodule Registry do
def start_link(options) do
keys = Keyword.get(options, :keys)
if keys not in @keys do
unless keys in @keys do
raise ArgumentError,
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
end
@@ -350,27 +350,27 @@ defmodule Registry do
meta = Keyword.get(options, :meta, [])
if not Keyword.keyword?(meta) do
unless Keyword.keyword?(meta) do
raise ArgumentError, "expected :meta to be a keyword list, got: #{inspect(meta)}"
end
partitions = Keyword.get(options, :partitions, 1)
if not (is_integer(partitions) and partitions >= 1) do
unless is_integer(partitions) and partitions >= 1 do
raise ArgumentError,
"expected :partitions to be a positive integer, got: #{inspect(partitions)}"
end
listeners = Keyword.get(options, :listeners, [])
if not (is_list(listeners) and Enum.all?(listeners, &is_atom/1)) do
unless is_list(listeners) and Enum.all?(listeners, &is_atom/1) do
raise ArgumentError,
"expected :listeners to be a list of named processes, got: #{inspect(listeners)}"
end
compressed = Keyword.get(options, :compressed, false)
if not is_boolean(compressed) do
unless is_boolean(compressed) do
raise ArgumentError,
"expected :compressed to be a boolean, got: #{inspect(compressed)}"
end
@@ -471,7 +471,7 @@ defmodule Registry do
"""
@doc since: "1.4.0"
@spec dispatch(registry, key, dispatcher, keyword) :: :ok
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [term()]}
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [any()]}
def dispatch(registry, key, mfa_or_fun, opts \\ [])
when is_atom(registry) and is_function(mfa_or_fun, 1)
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
@@ -609,65 +609,6 @@ defmodule Registry do
end
end
@doc """
Out-of-band locking of the given `lock_key` for the duration of `function`.
Only one function can execute under the same `lock_key` at a given
time. The given function always runs in the caller process.
The `lock_key` has its own namespace and therefore does not clash or
overlap with the regular registry keys. In other words, locking works
out-of-band from the regular Registry operations. See the "Use cases"
section below.
Locking behaves the same regardless of the registry type.
## Use cases
The Registry is safe and concurrent out-of-the-box. You are not required
to use this function when interacting with the Registry. Furthermore,
`Registry` with `:unique` keys can already act as a process-lock for any
given key. For example, you can ensure only one process runs at a given
time for a given `:key` by doing:
name = {:via, Registry, {MyApp.Registry, :key, :value}}
# Do not attempt to start if we are already running
if pid = GenServer.whereis(name) do
pid
else
case GenServer.start_link(__MODULE__, :ok, name: name) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
end
Process locking gives you plenty of flexibility and fault isolation and
is enough for most cases.
This function is useful only when spawning processes is not an option,
for example, when copying the data to another process could be too
expensive. Or when the work must be done within the current process
for other reasons. In such cases, this function provides a scalable
mechanism for managing locks on top of the registry's infrastructure.
## Examples
iex> Registry.start_link(keys: :unique, name: Registry.LockTest)
iex> Registry.lock(Registry.LockTest, :hello, fn -> :ok end)
:ok
iex> Registry.lock(Registry.LockTest, :world, fn -> self() end)
self()
"""
@doc since: "1.18.0"
def lock(registry, lock_key, function)
when is_atom(registry) and is_function(function, 0) do
{_kind, partitions, _, pid_ets, _} = info!(registry)
{pid_server, _pid_ets} = pid_ets || pid_ets!(registry, lock_key, partitions)
Registry.Partition.lock(pid_server, lock_key, function)
end
@doc """
Returns `{pid, value}` pairs under the given `key` in `registry` that match `pattern`.
@@ -1241,8 +1182,8 @@ defmodule Registry do
def count(registry) when is_atom(registry) do
case key_info!(registry) do
{_kind, partitions, nil} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
safe_size(key_ets!(registry, partition_index))
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
acc + safe_size(key_ets!(registry, partition_index))
end)
{_kind, 1, key_ets} ->
@@ -1317,8 +1258,9 @@ defmodule Registry do
:ets.select_count(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
count = :ets.select_count(key_ets!(registry, partition_index), spec)
acc + count
end)
end
end
@@ -1362,12 +1304,12 @@ defmodule Registry do
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) |> Enum.sort()
[{"hello", self(), :value}, {"world", self(), :value}]
If you want to get keys, you can pass a separate selector:
Get all keys in the registry:
iex> Registry.start_link(keys: :unique, name: Registry.SelectKeysTest)
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "world", :value)
iex> Registry.select(Registry.SelectKeysTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest)
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value)
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
["hello", "world"]
"""
@@ -1410,8 +1352,9 @@ defmodule Registry do
case key_info!(registry) do
{_kind, partitions, nil} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
count = :ets.select_count(key_ets!(registry, partition_index), spec)
acc + count
end)
{_kind, 1, key_ets} ->
@@ -1492,7 +1435,7 @@ defmodule Registry do
end
defp unlink_if_unregistered(pid_server, pid_ets, self) do
if not :ets.member(pid_ets, self) do
unless :ets.member(pid_ets, self) do
Process.unlink(pid_server)
end
end
@@ -1559,7 +1502,7 @@ defmodule Registry.Partition do
@moduledoc false
# This process owns the equivalent key and pid ETS tables
# and is responsible for linking to processes that map to
# and is responsible for monitoring processes that map to
# its own pid table.
use GenServer
@all_info -1
@@ -1584,25 +1527,13 @@ defmodule Registry.Partition do
@doc """
Starts the registry partition.
The process is only responsible for linking and cleaning up when processes crash.
The process is only responsible for monitoring, demonitoring
and cleaning up when monitored processes crash.
"""
def start_link(registry, arg) do
GenServer.start_link(__MODULE__, arg, name: registry)
end
@doc """
Runs function with a lock.
"""
def lock(pid, key, lock) do
ref = GenServer.call(pid, {:lock, key})
try do
lock.()
after
send(pid, {:unlock, key, ref})
end
end
## Callbacks
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
@@ -1623,7 +1554,7 @@ defmodule Registry.Partition do
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
end
{:ok, {pid_ets, %{}}}
{:ok, pid_ets}
end
# The key partition is a set for unique keys,
@@ -1657,21 +1588,7 @@ defmodule Registry.Partition do
{:reply, :ok, state}
end
def handle_call({:lock, key}, from, {ets, lock}) do
lock =
case lock do
%{^key => queue} ->
Map.put(lock, key, :queue.in(from, queue))
%{} ->
go(from, key)
Map.put(lock, key, :queue.new())
end
{:noreply, {ets, lock}}
end
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
def handle_info({:EXIT, pid, _reason}, ets) do
entries = :ets.take(ets, pid)
for {_pid, key, key_ets, _counter} <- entries do
@@ -1692,47 +1609,6 @@ defmodule Registry.Partition do
end
end
{:noreply, {ets, lock}}
end
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
unlock(key, state)
end
def handle_info({:unlock, key, ref}, state) do
Process.demonitor(ref, [:flush])
unlock(key, state)
end
defp unlock(key, {ets, lock}) do
%{^key => queue} = lock
lock =
case dequeue(queue, key) do
:empty -> Map.delete(lock, key)
{:not_empty, queue} -> Map.put(lock, key, queue)
end
{:noreply, {ets, lock}}
end
defp dequeue(queue, key) do
case :queue.out(queue) do
{:empty, _} ->
:empty
{{:value, {pid, _tag} = from}, queue} ->
if node(pid) != node() or Process.alive?(pid) do
go(from, key)
{:not_empty, queue}
else
dequeue(queue, key)
end
end
end
defp go({pid, _tag} = from, key) do
ref = Process.monitor(pid, tag: {:unlock, key})
GenServer.reply(from, ref)
{:noreply, ets}
end
end
+1 -1
View File
@@ -90,7 +90,7 @@ defmodule Set do
if target1 == target2 do
target1.intersection(set1, set2)
else
Enumerable.reduce(set1, {:cont, target1.new()}, fn v, acc ->
Enumerable.reduce(set1, {:cont, target1.new}, fn v, acc ->
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
end)
|> elem(1)
+146 -28
View File
@@ -31,11 +31,8 @@ defmodule Stream do
Due to their laziness, streams are useful when working with large
(or even infinite) collections. When chaining many operations with `Enum`,
intermediate lists are created, while `Stream` creates a recipe of
computations that are executed at a later moment. Then when the
stream is consumed later on, most commonly by using a function in
the `Enum` module, the stream will emit its elements one by one.
Let's see another example:
computations that are executed at a later moment. Let's see another
example:
1..3
|> Enum.map(&IO.inspect(&1))
@@ -93,21 +90,11 @@ defmodule Stream do
This module also provides many convenience functions for creating streams,
like `Stream.cycle/1`, `Stream.unfold/2`, `Stream.resource/3` and more.
> #### Do not check for `Stream` structs
>
> While some functions in this module may return the `Stream` struct,
> you must never explicitly check for the `Stream` struct, as streams
> may come in several shapes, such as `IO.Stream`, `File.Stream`, or
> even `Range`s.
>
> The functions in this module only guarantee to return enumerables
> and their implementation (structs, anonymous functions, etc) may
> change at any time. For example, a function that returns an anonymous
> function today may return a struct in future releases.
>
> Instead of checking for a particular type, you must instead write
> assertive code that assumes you have an enumerable, using the functions
> in the `Enum` or `Stream` module accordingly.
Note the functions in this module are guaranteed to return enumerables.
Since enumerables can have different shapes (structs, anonymous functions,
and so on), the functions in this module may return any of those shapes
and this may change at any time. For example, a function that today
returns an anonymous function may return a struct in future releases.
"""
@doc false
@@ -881,7 +868,7 @@ defmodule Stream do
"""
@spec transform(Enumerable.t(), acc, fun) :: Enumerable.t()
when fun: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
acc: term
acc: any
def transform(enum, acc, reducer) when is_function(reducer, 2) do
&do_transform(enum, fn -> acc end, reducer, &1, &2, nil, fn acc -> acc end)
end
@@ -896,7 +883,7 @@ defmodule Stream do
when start_fun: (-> acc),
reducer: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
after_fun: (acc -> term),
acc: term
acc: any
def transform(enum, start_fun, reducer, after_fun)
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(after_fun, 1) do
&do_transform(enum, start_fun, reducer, &1, &2, nil, after_fun)
@@ -923,7 +910,7 @@ defmodule Stream do
reducer: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
last_fun: (acc -> {Enumerable.t(), acc} | {:halt, acc}),
after_fun: (acc -> term),
acc: term
acc: any
def transform(enum, start_fun, reducer, last_fun, after_fun)
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(last_fun, 1) and
is_function(after_fun, 1) do
@@ -963,8 +950,6 @@ defmodule Stream do
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
{_, vals} ->
# Do not attempt to call the resource again, it has either done or halted
next = fn _ -> {:done, []} end
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
end
end
@@ -977,6 +962,7 @@ defmodule Stream do
last_fun.(user_acc)
catch
kind, reason ->
next.({:halt, []})
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
@@ -1375,8 +1361,140 @@ defmodule Stream do
@doc since: "1.12.0"
@spec zip_with(enumerables, (Enumerable.t() -> term)) :: Enumerable.t()
when enumerables: [Enumerable.t()] | Enumerable.t()
def zip_with(enumerables, zip_fun) do
R.zip_with(enumerables, zip_fun)
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
&zip_list(enumerables, &1, &2, zip_fun)
else
&zip_enum(enumerables, &1, &2, zip_fun)
end
end
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
{:halted, acc}
end
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
end
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
case zip_list_heads_tails(enumerables, [], []) do
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
:error -> {:done, acc}
end
end
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
zip_list_heads_tails(rest, [head | heads], [tail | tails])
end
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
:error
end
defp zip_list_heads_tails([], heads, tails) do
{:lists.reverse(heads), :lists.reverse(tails)}
end
defp zip_enum(enumerables, acc, fun, zip_fun) do
step = fn x, acc ->
{:suspend, :lists.reverse([x | acc])}
end
enum_funs =
Enum.map(enumerables, fn enum ->
{&Enumerable.reduce(enum, &1, step), [], :cont}
end)
do_zip_enum(enum_funs, acc, fun, zip_fun)
end
# This implementation of do_zip_enum/4 works for any number of streams to zip
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
do_zip_close(zips)
{:halted, acc}
end
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
end
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
{:done, acc}
end
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
try do
do_zip_next(zips, acc, callback, [], [], zip_fun)
catch
kind, reason ->
do_zip_close(zips)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:next, buffer, acc} ->
do_zip_enum(buffer, acc, callback, zip_fun)
{:done, _acc} = other ->
other
end
end
# do_zip_next/6 computes the next tuple formed by
# the next element of each zipped stream.
defp do_zip_next(
[{_, [], :halt} | zips],
acc,
_callback,
_yielded_elems,
buffer,
_zip_fun
) do
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
case fun.({:cont, []}) do
{:suspended, [elem | next_acc], fun} ->
next_buffer = [{fun, next_acc, :cont} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, [elem | next_acc]} ->
next_buffer = [{fun, next_acc, :halt} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, []} ->
# The current zipped stream terminated, so we close all the streams
# and return {:halted, acc} (which is returned as is by do_zip/3).
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
end
defp do_zip_next(
[{fun, zip_acc, zip_op} | zips],
acc,
callback,
yielded_elems,
buffer,
zip_fun
) do
[elem | rest] = zip_acc
next_buffer = [{fun, rest, zip_op} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
end
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
# "yielded_elems" is a reversed list of results for the current iteration of
# zipping. That is to say, the nth element from each of the enums being zipped.
# It needs to be reversed and passed to the zipping function so it can do it's thing.
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
end
defp do_zip_close(zips) do
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
end
## Sources
@@ -1444,7 +1562,7 @@ defmodule Stream do
defp check_cycle_first_element(reduce) do
fn acc ->
case reduce.(acc) do
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
{state, []} when state in [:done, :halted] ->
raise ArgumentError, "cannot cycle over an empty enumerable"
other ->
+3 -139
View File
@@ -2,142 +2,6 @@ defmodule Stream.Reducers do
# Collection of reducers and utilities shared by Enum and Stream.
@moduledoc false
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
&zip_list(enumerables, &1, &2, zip_fun)
else
&zip_enum(enumerables, &1, &2, zip_fun)
end
end
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
{:halted, acc}
end
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
end
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
case zip_list_heads_tails(enumerables, [], []) do
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
:error -> {:done, acc}
end
end
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
zip_list_heads_tails(rest, [head | heads], [tail | tails])
end
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
:error
end
defp zip_list_heads_tails([], heads, tails) do
{:lists.reverse(heads), :lists.reverse(tails)}
end
defp zip_enum(enumerables, acc, fun, zip_fun) do
step = fn x, acc ->
{:suspend, :lists.reverse([x | acc])}
end
enum_funs =
Enum.map(enumerables, fn enum ->
{&Enumerable.reduce(enum, &1, step), [], :cont}
end)
do_zip_enum(enum_funs, acc, fun, zip_fun)
end
# This implementation of do_zip_enum/4 works for any number of streams to zip
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
do_zip_close(zips)
{:halted, acc}
end
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
end
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
{:done, acc}
end
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
try do
do_zip_next(zips, acc, callback, [], [], zip_fun)
catch
kind, reason ->
do_zip_close(zips)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:next, buffer, acc} ->
do_zip_enum(buffer, acc, callback, zip_fun)
{:done, _acc} = other ->
other
end
end
# do_zip_next/6 computes the next tuple formed by
# the next element of each zipped stream.
defp do_zip_next(
[{_, [], :halt} | zips],
acc,
_callback,
_yielded_elems,
buffer,
_zip_fun
) do
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
case fun.({:cont, []}) do
{:suspended, [elem | next_acc], fun} ->
next_buffer = [{fun, next_acc, :cont} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, [elem | next_acc]} ->
next_buffer = [{fun, next_acc, :halt} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, []} ->
# The current zipped stream terminated, so we close all the streams
# and return {:halted, acc} (which is returned as is by do_zip/3).
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
end
defp do_zip_next(
[{fun, zip_acc, zip_op} | zips],
acc,
callback,
yielded_elems,
buffer,
zip_fun
) do
[elem | rest] = zip_acc
next_buffer = [{fun, rest, zip_op} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
end
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
# "yielded_elems" is a reversed list of results for the current iteration of
# zipping. That is to say, the nth element from each of the enums being zipped.
# It needs to be reversed and passed to the zipping function so it can do it's thing.
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
end
defp do_zip_close(zips) do
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
end
def chunk_every(chunk_by, enumerable, count, step, leftover) do
limit = :erlang.max(count, step)
@@ -287,10 +151,10 @@ defmodule Stream.Reducers do
defmacro reject(callback, fun \\ nil) do
quote do
fn entry, acc ->
if unquote(callback).(entry) do
skip(acc)
else
unless unquote(callback).(entry) do
next(unquote(fun), entry, acc)
else
skip(acc)
end
end
end
+15 -41
View File
@@ -18,7 +18,7 @@ defmodule String do
"hello world"
The functions in this module act according to
[The Unicode Standard, Version 16.0.0](http://www.unicode.org/versions/Unicode16.0.0/).
[The Unicode Standard, Version 15.1.0](http://www.unicode.org/versions/Unicode15.1.0/).
## Interpolation
@@ -326,7 +326,7 @@ defmodule String do
recur_printable?(string, character_limit)
end
defp recur_printable?(<<_::binary>>, 0), do: true
defp recur_printable?(_string, 0), do: true
defp recur_printable?(<<>>, _character_limit), do: true
for char <- 0x20..0x7E do
@@ -357,10 +357,9 @@ defmodule String do
@doc ~S"""
Divides a string into substrings at each Unicode whitespace
occurrence with leading and trailing whitespace ignored.
Groups of whitespace are treated as a single occurrence.
Divisions do not occur on non-breaking whitespace.
occurrence with leading and trailing whitespace ignored. Groups
of whitespace are treated as a single occurrence. Divisions do
not occur on non-breaking whitespace.
## Examples
@@ -376,11 +375,6 @@ defmodule String do
iex> String.split("no\u00a0break")
["no\u00a0break"]
Removes empty strings, like when using `trim: true` in `String.split/3`.
iex> String.split(" ")
[]
"""
@spec split(t) :: [t]
defdelegate split(binary), to: String.Break
@@ -488,15 +482,6 @@ defmodule String do
iex> String.split(String.normalize("é", :nfc), "e")
["é"]
When using both the `:trim` and the `:parts` option, the empty values
are removed as the parts are computed (if any). No trimming happens
after all parts are computed:
iex> String.split(" a b c ", " ", trim: true, parts: 2)
["a", " b c "]
iex> String.split(" a b c ", " ", trim: true, parts: 3)
["a", "b", " c "]
"""
@spec split(t, pattern | Regex.t(), keyword) :: [t]
def split(string, pattern, options \\ [])
@@ -655,12 +640,10 @@ defmodule String do
The offset is capped to the length of the string. Returns a tuple with
two elements.
> #### Linear Access {: .warning}
>
> This function splits on graphemes and for such it has to linearly traverse
> the string.
> If you want to split a string or a binary based on the number of bytes,
> use `Kernel.binary_part/3` instead.
Note: keep in mind this function splits on graphemes and for such it
has to linearly traverse the string. If you want to split a string or
a binary based on the number of bytes, use `Kernel.binary_part/3`
instead.
## Examples
@@ -2242,12 +2225,6 @@ defmodule String do
Returns the grapheme at the `position` of the given UTF-8 `string`.
If `position` is greater than `string` length, then it returns `nil`.
> #### Linear Access {: .warning}
>
> This function has to linearly traverse the string.
> If you want to access a string or a binary in constant time based on the
> number of bytes, use `Kernel.binary_slice/3` or `:binary.at/2` instead.
## Examples
iex> String.at("elixir", 0)
@@ -2984,9 +2961,9 @@ defmodule String do
@doc """
Returns a float whose text representation is `string`.
`string` must be the string representation of a float including leading digits and a decimal
point. To parse a string without decimal point as a float, refer to `Float.parse/1`. Otherwise,
an `ArgumentError` will be raised.
`string` must be the string representation of a float including a decimal point.
In order to parse a string without decimal point as a float then `Float.parse/1`
should be used. Otherwise, an `ArgumentError` will be raised.
Inlined by the compiler.
@@ -3001,9 +2978,6 @@ defmodule String do
String.to_float("3")
** (ArgumentError) argument error
String.to_float(".3")
** (ArgumentError) argument error
"""
@spec to_float(String.t()) :: float
def to_float(string) when is_binary(string) do
@@ -3069,10 +3043,10 @@ defmodule String do
end
defp bag_difference(bag1, bag2) do
Enum.sum_by(bag1, fn {char, count1} ->
Enum.reduce(bag1, 0, fn {char, count1}, sum ->
case bag2 do
%{^char => count2} -> max(count1 - count2, 0)
%{} -> count1
%{^char => count2} -> sum + max(count1 - count2, 0)
%{} -> sum + count1
end
end)
end
+1 -1
View File
@@ -57,6 +57,6 @@ end
defimpl String.Chars, for: Float do
def to_string(term) do
Float.to_string(term)
:erlang.float_to_binary(term, [:short])
end
end
-6
View File
@@ -188,12 +188,6 @@ defmodule StringIO do
{:noreply, state}
end
# Fail fast if someone tries to use it with a File API
def handle_info({:file_request, from, reply_as, _req}, state) do
send(from, {:file_reply, reply_as, {:error, :enotsup}})
{:noreply, state}
end
def handle_info({:DOWN, _, _, _, _}, state) do
{:stop, :shutdown, state}
end
+1 -1
View File
@@ -525,7 +525,7 @@ defmodule Supervisor do
import Supervisor.Spec
@behaviour Supervisor
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
+1 -1
View File
@@ -168,7 +168,7 @@ defmodule Supervisor.Spec do
) :: {:ok, tuple}
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
def supervise(children, options) do
if !(strategy = options[:strategy]) do
unless strategy = options[:strategy] do
raise ArgumentError, "expected :strategy option to be given"
end
+1 -1
View File
@@ -1101,7 +1101,7 @@ defmodule System do
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
assert_no_null_byte!(command, "System.cmd/3")
if not Enum.all?(args, &is_binary/1) do
unless Enum.all?(args, &is_binary/1) do
raise ArgumentError, "all arguments for System.cmd/3 must be binaries"
end
+5 -5
View File
@@ -329,7 +329,7 @@ defmodule Task do
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -610,7 +610,7 @@ defmodule Task do
* `:zip_input_on_exit` - (since v1.14.0) adds the original
input to `:exit` tuples. The value emitted for that task is
`{:exit, {input, reason}}`, where `input` is the collection element
that caused an exit during processing. Defaults to `false`.
that caused an exited during processing. Defaults to `false`.
## Example
@@ -716,7 +716,7 @@ defmodule Task do
iex> strings = ["long string", "longer string", "there are many of these"]
iex> stream = Task.async_stream(strings, fn text -> text |> String.codepoints() |> Enum.count() end)
iex> Enum.sum_by(stream, fn {:ok, num} -> num end)
iex> Enum.reduce(stream, 0, fn {:ok, num}, acc -> num + acc end)
47
See `async_stream/5` for discussion, options, and more examples.
@@ -842,14 +842,14 @@ defmodule Task do
Process.demonitor(ref, [:flush])
{url, state} = pop_in(state.tasks[ref])
IO.puts("Got #{inspect(result)} for URL #{inspect url}")
IO.puts "Got #{inspect(result)} for URL #{inspect url}"
{:noreply, state}
end
# If the task fails...
def handle_info({:DOWN, ref, _, _, reason}, state) do
{url, state} = pop_in(state.tasks[ref])
IO.puts("URL #{inspect url} failed with reason #{inspect(reason)}")
IO.puts "URL #{inspect url} failed with reason #{inspect(reason)}"
{:noreply, state}
end
end
+3 -3
View File
@@ -209,15 +209,15 @@ defmodule Task.Supervised do
ordered = Keyword.get(options, :ordered, true)
zip_input_on_exit = Keyword.get(options, :zip_input_on_exit, false)
if not (is_integer(max_concurrency) and max_concurrency > 0) do
unless is_integer(max_concurrency) and max_concurrency > 0 do
raise ArgumentError, ":max_concurrency must be an integer greater than zero"
end
if on_timeout not in [:exit, :kill_task] do
unless on_timeout in [:exit, :kill_task] do
raise ArgumentError, ":on_timeout must be either :exit or :kill_task"
end
if not ((is_integer(timeout) and timeout >= 0) or timeout == :infinity) do
unless (is_integer(timeout) and timeout >= 0) or timeout == :infinity do
raise ArgumentError, ":timeout must be either a positive integer or :infinity"
end
+3 -3
View File
@@ -74,7 +74,7 @@ defmodule Task.Supervisor do
@typedoc "Option values used by `start_link`"
@type option ::
GenServer.option()
DynamicSupervisor.option()
| DynamicSupervisor.init_option()
@typedoc """
@@ -498,7 +498,7 @@ defmodule Task.Supervisor do
Starts a task as a child of the given `supervisor`.
Task.Supervisor.start_child(MyTaskSupervisor, fn ->
IO.puts("I am running in a task")
IO.puts "I am running in a task"
end)
Note that the spawned process is not linked to the caller, but
@@ -614,7 +614,7 @@ defmodule Task.Supervisor do
defp build_stream(supervisor, link_type, enumerable, fun, options) do
shutdown = Keyword.get(options, :shutdown, 5000)
if not ((is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill) do
unless (is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill do
raise ArgumentError, ":shutdown must be either a positive integer or :brutal_kill"
end
+16 -2
View File
@@ -87,8 +87,22 @@ defmodule Tuple do
:erlang.insert_element(index + 1, tuple, value)
end
@doc false
@deprecated "Use insert_at instead"
@doc """
Inserts an element at the end of a tuple.
Returns a new tuple with the element appended at the end, and contains
the elements in `tuple` followed by `value` as the last element.
Inlined by the compiler.
## Examples
iex> tuple = {:foo, :bar}
iex> Tuple.append(tuple, :baz)
{:foo, :bar, :baz}
"""
@spec append(tuple, term) :: tuple
def append(tuple, value) do
:erlang.append_element(tuple, value)
end
+16 -21
View File
@@ -367,27 +367,21 @@ defmodule URI do
@doc """
Percent-encodes all characters that require escaping in `string`.
The optional `predicate` argument specifies a function used to detect whether
a byte in the `string` should be escaped:
By default, this function is meant to escape the whole URI, and
therefore it will only escape characters which are foreign in
all parts of a URI. Reserved characters (such as `:` and `/`)
or unreserved (such as letters and numbers) are not escaped.
* if the function returns a truthy value, the byte should be kept as-is.
* if the function returns a falsy value, the byte should be escaped.
Because different components of a URI require different escaping
rules, this function also accepts a `predicate` function as an optional
argument. If passed, this function will be called with each byte
in `string` as its argument and should return a truthy value (anything other
than `false` or `nil`) if the given byte should be left as is, or
return a falsy value (`false` or `nil`) if the character should be
escaped. Defaults to `URI.char_unescaped?/1`.
The `predicate` argument can use some built-in functions:
* `URI.char_unescaped?/1` (default) - reserved characters (such as `:`
and `/`) or unreserved (such as letters and numbers) are kept as-is.
It's typically used to encode the whole URI.
* `URI.char_unreserved?/1` - unreserved characters (such as letters and
numbers) are kept as-is. It's typically used to encode components in
a URI, such as query or fragment.
* `URI.char_reserved?/1` - Reserved characters (such as `:` and `/`) are
kept as-is.
And, you can also use custom functions.
See `encode_www_form/1` if you are interested in encoding `string` as
"x-www-form-urlencoded".
See `encode_www_form/1` if you are interested in escaping reserved
characters too.
## Examples
@@ -699,7 +693,8 @@ defmodule URI do
> #### `:authority` field {: .info}
>
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
> This function sets the field `:authority` for backwards-compatibility reasons
> but it is deprecated.
## Examples
@@ -907,7 +902,7 @@ defmodule URI do
%{rel | path: remove_dot_segments_from_path(rel.path)}
end
def merge(%URI{} = base, %URI{host: host} = rel) when host != nil do
def merge(base, %URI{host: host} = rel) when host != nil do
%{rel | scheme: base.scheme, path: remove_dot_segments_from_path(rel.path)}
end
+1 -1
View File
@@ -686,7 +686,7 @@ end
defimpl Inspect, for: Version.Requirement do
def inspect(%Version.Requirement{source: source}, opts) do
colorized = Inspect.Algebra.color_doc("\"" <> source <> "\"", :string, opts)
colorized = Inspect.Algebra.color("\"" <> source <> "\"", :string, opts)
Inspect.Algebra.concat(["Version.parse_requirement!(", colorized, ")"])
end

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