Compare commits

..
101 Commits
Author SHA1 Message Date
José Valim e0220bd267 Revert "Add branch docs publishing"
This reverts commit 6f5cc529d5.

Latest ExDoc no longer works with this version.
2023-05-30 11:49:19 +02:00
José Valim 6f5cc529d5 Add branch docs publishing 2023-05-30 11:43:21 +02:00
Alberto Sartori 3d80256852 Fix invalid example for using erlang logger handler (#10432) 2020-10-16 12:54:08 +02:00
tarreeandSamuel Smith 8731ef03d9 Adjust @type body for Registry to match erlang ets match_spec Result (#10017)
Co-authored-by: Samuel Smith <samuel.smith@se.com>
2020-07-14 12:33:39 +02:00
José Valim f8a2d05b57 Clarify to_unix docs 2020-07-14 12:33:25 +02:00
José Valim 1145dc0168 Release v1.10.4 2020-07-04 13:10:47 +02:00
José Valim 12f93df04f Revert "Add --repo to archive.install and escript.install (#10121)"
This reverts commit e9ea9dea7f.
2020-07-01 15:40:55 +02:00
José Valim 47a63a73b6 Update CHANGELOG 2020-07-01 15:40:51 +02:00
Cleidiano Oliveira d0ebaa7cdd Allow user to define a type named union (#10055) 2020-07-01 15:26:12 +02:00
Wojtek Mach e9ea9dea7f Add --repo to archive.install and escript.install (#10121) 2020-06-22 17:12:45 +02:00
Mitchell Henke 1feef9cb34 include callers in translated Logger metadata for Task (#10111) 2020-06-19 20:51:21 +02:00
Eksperimental 5c5fa57234 Update .cirrus.yml to latest Erlang/OTP versions and changes from master (#10101) 2020-06-15 19:36:55 +02:00
Wojtek Mach e891b1a831 Don't add compile-time dependency on defdelegate (#10093)
The dependency doesn't seem needed in this case.

We go through extra hoops to maintain dynamic invocations where
`opts` is not a compile-time list and :to is not a literal.

An easy way to see this optimization is:

    defmodule A do
      defdelegate f(), to: B
    end

    defmodule B do
      def f() do
        :ok
      end
    end

    $ touch lib/b.ex && mix compile --verbose
    Compiling 1 file (.ex)
    Compiled lib/b.ex

vs:

    defmodule A do
      opts = [to: B]
      defdelegate f(), opts
    end

    defmodule B do
      def f() do
        :ok
      end
    end

    $ touch lib/b.ex && mix compile --verbose
    Compiling 2 files (.ex)
    Compiled lib/b.ex
    Compiled lib/a.ex
2020-06-11 16:02:19 +02:00
José Valim 001a5eb2e8 Add line numbers to deprecated imports 2020-06-11 16:01:46 +02:00
José Valim 6085ed0288 Do not diff guards if pattern does not match 2020-06-04 20:15:35 +02:00
José Valim 57be943f91 Do not raise on foreign types, closes #10077 2020-06-02 15:06:29 +02:00
Waldemar Rachwał 99cad598a8 Fix read of app file located in archive .ez file (#10062) 2020-05-27 17:59:14 +02:00
José Valim ef4c926299 Provide exception blaming to linked and trapped exits in ExUnit 2020-05-22 11:00:46 +02:00
José Valim d1acad0155 Add extra suggestions when Hex can't be installed, closes #10039 2020-05-18 10:05:09 +02:00
Dallin Osmun 4a69a00c71 Report the correct line number when raising inside a macro (#10040) 2020-05-16 09:33:15 +02:00
José Valim a9ad1f1b2a v1.10.3 is compatible with Erlang/OTP 23 2020-05-13 12:54:37 +02:00
Wojtek Mach 53e3d8f55e Add :cd option to Mix.Shell.cmd/2 and --cd to mix cmd (#10029) 2020-05-13 09:48:29 +02:00
Ryan Zidago f53b8c822d Fix Task PID and caller in Task Supervisor reports (#10025)
Closes #10024
2020-05-09 01:14:29 +02:00
José Valim cdd312b0ca Support proper encoding in releases config (#10012) 2020-05-01 20:14:18 +02:00
José Valim 0cc53735a1 Force modules to be loaded when they are required 2020-04-26 11:02:46 +02:00
José Valim c88ecdec16 Release v1.10.3 2020-04-25 10:30:20 +02:00
José Valim 1b4536bf17 Fix improper regex use, closes #9914 2020-04-25 10:25:43 +02:00
José Valim ab4d6817ab Run the formatter 2020-04-24 18:56:40 +02:00
José Valim 717490a1b0 Respect :path when tar-ing releases, closes #9949 2020-04-24 18:11:22 +02:00
José Valim 8cb1361e1a Fix diffing for variable structs, closes #9958 2020-04-23 21:40:14 +02:00
Dave Cottlehuber 967f4b412c Update build to a supported FreeBSD version (#9944) 2020-04-23 21:00:45 +02:00
José Valim 8a876d6c78 Do not assume all files have started to compile, closes #9943 2020-04-23 20:09:02 +02:00
José Valim 831b12a116 Do not attempt to load in memory beam files, closes #9788 2020-04-23 15:59:08 +02:00
José Valim d16497b966 Bring back String.normalize and fix infinite loop 2020-04-23 13:35:59 +02:00
José Valim 7ef6dfc4e4 Fixes to ExUnit.Diff, closes #9926 2020-04-21 20:24:14 +02:00
José Valim e1158a55a3 Revert "Use an empty keyword list instead of nil for empty definitions"
This reverts commit 4477115675.
2020-03-24 20:22:00 +01:00
Eric Meadows-Jönsson 78cc8bea7a Fix divergedreq error when update changes requirement and version (#9811)
The fix is to mark the dependency for compilation in the Fetcher (by
touching .fetch) so that the Loader wont read the version from the .app
file.
2020-03-23 11:59:22 +01:00
José Valim 5914aba7e4 Add missing CHANGELOG entry 2020-03-22 20:59:21 +01:00
José Valim 40301963e4 Skip sys_core_prepare pass on Erlang/OTP 23+ 2020-03-18 14:22:54 +01:00
José Valim 248d532ae4 Compare pins as values, closes #9872 2020-03-13 11:52:04 +01:00
José Valim 74bc9506a9 Reduce the newlines count when they are immediately followed by a comment, closes #9873 2020-03-13 11:52:02 +01:00
Fernando Tapia Rico 41e218b160 Document version of :start/:stop events (tracers) (#9871) 2020-03-13 09:58:21 +01:00
José Valim 94221f2894 Shutdowns can be any term, closes #9878 2020-03-13 09:58:00 +01:00
José Valim 6eaac2dc72 Notify the script has been tested for bash, closes #9890 2020-03-13 09:57:25 +01:00
José Valim 92650c84be Update CHANGELOG, closes #9887 2020-03-09 22:05:26 +01:00
José Valim b30cbb600b Add start and stop events to compilation tracers, closes #9868 2020-03-02 14:36:32 +01:00
Wojtek Mach 6092fb4618 Return [{mod, bin}] from Code.compile_file/2, require_file/2, load_file/2 (#9864)
This was the documented behaviour on 1.9 that was broken on v1.10: (version `git` is this branch)

    $ cat a.ex
    defmodule A, do: nil

    $ for v in 1.9.4-otp-22 1.10.2-otp-22 git; do asdf local elixir $v && echo $v; elixir -e 'IO.inspect Code.compile_file("a.ex")'; done
    1.9.4-otp-22
    [{A, <<70, 79, ...>>}
    1.10.2-otp-22
    [{A, %{attributes: [], ...}]
    git
    [{A, <<70, 79, ...>>}

    $ for v in 1.9.4-otp-22 1.10.2-otp-22 git; do asdf local elixir $v && echo $v; elixir -e 'IO.inspect Code.require_file("a.ex")'; done
    1.9.4-otp-22
    [{A, <<70, 79, ...>>}]
    1.10.2-otp-22
    [{A, %{attributes: [], ...}]
    git
    [{A, <<70, 79, ...>>}]

    $ for v in 1.9.4-otp-22 1.10.2-otp-22 git; do asdf local elixir $v && echo $v; elixir -e 'IO.inspect Code.load_file("a.ex")'; done
    1.9.4-otp-22
    [{A, <<70, 79, ...>>}]
    1.10.2-otp-22
    warning: Code.load_file/1 is deprecated. Use Code.require_file/2 or Code.compile_file/2 instead
      nofile:1

    [{A, %{attributes: [], ...}]
    git
    warning: Code.load_file/1 is deprecated. Use Code.require_file/2 or Code.compile_file/2 instead
      nofile:1

    [{A, <<70, 79, ...>>}]
2020-03-01 00:50:31 +01:00
José Valim 07450832e7 Release v1.10.2 2020-02-26 12:53:01 +01:00
José Valim 34adbcb95a Fix unused variable warning 2020-02-26 12:17:23 +01:00
José Valim 286d310ac3 Fixes for Erlang/OTP 23 2020-02-26 12:05:47 +01:00
José Valim 2fc916088a Rewrite stacktraces for compatibility with upcoming OTP version 2020-02-24 21:36:51 +01:00
José Valim 9a428e9cdd Fix bootstrap 2020-02-13 12:16:15 +01:00
José Valim 090dddcf00 Only escape contents outside of interpolation, closes #9812 2020-02-12 20:51:58 +01:00
José Valim 089c470e13 Only print metadata of known types, closes #9814 2020-02-12 19:03:59 +01:00
José Valim 68481b7377 Do not crash on fake down messages on stream monitor, closes #9803 2020-02-12 18:31:25 +01:00
José Valim e2dc9ae72e Do not write files to disk if warnings as errors was triggered, closes #9808 2020-02-12 18:00:50 +01:00
José Valim 51c95b6000 Release v1.10.1 2020-02-10 12:42:26 +01:00
José Valim 12917b00e5 Unpack extra guards from with matches, closes #9801 2020-02-09 18:15:43 +01:00
Łukasz Jan Niemier ab9ecf2d7f fix(logger): use correct return value from report_cb/2 (#9798)
Earlier it assumed that the return type is the same as in `report_cb/1`
which is not true. `report_cb/1` returns format string and data while
`report_cb/2` returns already formatted string.
2020-02-09 09:35:02 +01:00
José Valim eca809285e Ensure assert_received still produces valid exceptions, closes #9799 2020-02-09 09:32:40 +01:00
José Valim 8e71e65aae Fix typo 2020-02-08 08:31:09 +01:00
José Valim c3a27be073 Do not imply nil is caused by Access 2020-02-08 01:58:44 +01:00
José Valim e10f9fb6fe Revert "Emit warnings for unused match vars in assertions, closes #9787"
Revert it as we may have false positives in v1.10.
We are still keeping it on master.

This reverts commit 696bad8de1.
2020-02-06 20:26:29 +01:00
José Valim f66ffd0733 Add missing :erlang.size/1 guard, closes #9789 2020-02-06 18:45:01 +01:00
José Valim 696bad8de1 Emit warnings for unused match vars in assertions, closes #9787 2020-02-06 18:35:21 +01:00
Joel C 96c8e31410 Quote variables in cli_bat_text install section (#9780)
This allows the release batch script to be run in a path that
includes spaces.
2020-02-04 14:45:46 +01:00
José Valim 70be440359 Only warn for commas at the end of words 2020-02-04 13:24:50 +01:00
José Valim c6d6683225 Add tests for timer and interval with infinity 2020-02-03 12:41:54 +01:00
José Valim db2504eca6 Do not emit invalid code when formatting assocs in maps, closes #9771 2020-02-01 11:18:42 +01:00
José Valim 98c3f5c602 Do not validate line in quote (moved to v1.11) 2020-01-31 11:23:25 +01:00
Akash Hiremath db4684b2f5 Fix logger truncate for multi-byte characters (#9767) 2020-01-31 10:44:45 +01:00
José Valim f3f65e4cd1 No longer rebroadcast messages as this is taken care by Erlang's logger
Closes #9765.
2020-01-30 17:02:43 +01:00
Piotr Szmielew 5331174934 Allow using remote or rpc with compile_env
Signed-off-by: José Valim <jose.valim@dashbit.co>
2020-01-30 12:05:47 +01:00
Devon Estes f4f2be67d7 Fix docs for compiler tracing events (#9760)
The `local_function` event is a tuple with four elements, not five.
2020-01-30 11:44:12 +01:00
Theo Dowling 686daadaaa Fix typo in CHANGELOG (#9757) 2020-01-28 07:37:53 +01:00
José Valim 6e8563e930 Add space to quote error message 2020-01-27 12:50:23 +01:00
José Valim 65be24a8c2 CHANGELOG fixes 2020-01-27 10:24:23 +01:00
José Valim 5bd7a90864 Release v1.10 2020-01-27 10:09:33 +01:00
Deniz Adrian 831046fcb7 Ensure ERTS is not copied into release (#9752)
The default behaviour when starting a new mix project is to
`include_erts: true`.

If at some point you choose to `include_erts: false`, but have the ERTS
artifact hanging around from previous builds, it gets included in future
builds, therefore rendering `include_erts: false` broken when running on
systems that have built with `include_erts: true` for the same
environment.

`make_tar/1` currently always tries to include the ERTS artifact into
the archive, regardless of our `include_erts` configuration. this fails
in 1.9.4 if no previous artifact is present, and seems to be worked
around in the current implementation by simply ignoring source
directories that don't exist.

This adds a regression test for the behaviour by "simulating" an ERTS
leftover from a previous build.

This also adds logic to not try to include ERTS into archives in the
first place when our release is configured with `include_erts: false`.
2020-01-25 15:44:01 +01:00
José Valim 6c44feae2f Update CHANGELOG 2020-01-24 19:23:51 +01:00
José Valim 35eef87590 Allow file, line and context to be dynamically set on quote, closes #9721 2020-01-23 22:49:31 +01:00
Eric Meadows-Jönsson fa091c6bd9 Only error for duplicate map keys in matches (#9748)
In matches we will error:

    %{a: :b, a: :c} = %{a: :b}

In normal expressions we will only warn:

    %{a: :b, a: :c}
2020-01-23 20:29:24 +01:00
José Valim a561f81539 Update return types in docs for Mix.Generator functions, closes #9749 2020-01-23 14:40:07 +01:00
José Valim 739c1b6292 Keep existing APIs for min/max/min_by/max_by 2020-01-17 12:54:11 +01:00
Fernando Tapia Rico 3e1e6ddf8a Update Library Guidelines.md (#9739) 2020-01-15 19:00:43 +01:00
José Valim ccb8c5a2e7 Add a new anti-pattern section to library guidelines 2020-01-15 17:55:56 +01:00
Eric Meadows-Jönsson 07e580cf17 Keep line number in cond_clause error (#9730) 2020-01-14 17:54:40 +01:00
Jason Axelson 1f9f713d9c Document Mix.Config.persist soft deprecation (#9722) 2020-01-13 15:51:25 +01:00
José Valim 9814a4cd61 Do not expand interpolation twice, closes #9709 2020-01-12 17:22:45 +01:00
José Valim 3dfdd80dff Make sure to traverse args in xref 2020-01-12 16:25:13 +01:00
José Valim 0fdc7510b8 Remove guard checks on is_odd/is_even as those are tested in defguard, closes #9700 2020-01-12 16:00:12 +01:00
Eric Meadows-Jönsson 0e8873238d Do not warn for functions generated by Erlang compiler (#9713) 2020-01-12 15:42:54 +01:00
Eric Meadows-Jönsson ba02eb90a0 Allow any type on the RHS of or/2 (#9706)
Closes #9704.
2020-01-11 12:19:29 +01:00
Eric Meadows-Jönsson fdc7716bf3 Improve type check error message (#9705)
A type error may not always cause a clause to fail.

Related to #9700.
2020-01-11 12:19:29 +01:00
José Valim a050c00fbc Use meta on rewrites, closes #9714 2020-01-11 12:17:43 +01:00
Bram Verburg d395c3e3cf Update CHANGELOG.md (#9708) 2020-01-09 12:28:17 +01:00
Tobiasz Małecki c3e903f339 Fix mix.release raising error when src is missing in erts_source (#9703) 2020-01-09 09:57:03 +01:00
José Valim 452ba40855 Update CHANGELOG 2020-01-08 16:56:36 +01:00
José Valim 8854492cc5 Also remove undefined warnings when loading projects outside the CLI 2020-01-08 11:52:26 +01:00
José Valim 105770cfa0 Release v1.10.0-rc.0 2020-01-07 15:10:04 +01:00
José Valim 28b053a122 Prepare v1.10 release 2020-01-07 15:03:35 +01:00
332 changed files with 7640 additions and 17377 deletions
+117 -1
View File
@@ -2,13 +2,129 @@ env:
CIRRUS_CLONE_DEPTH: 50
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
ERLC_OPTS: "+warnings_as_errors"
LANG: C.UTF-8
test_template: &DEFAULT_TEST_SETTINGS
# don't cancel the task execution if it's master or a release branch
auto_cancellation: $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH !=~ 'v\d+\.\d+.*'
test_linux_task:
<<: *DEFAULT_TEST_SETTINGS
container:
image: buildpack-deps:trusty
cpu: 8
memory: 1536Mi
env:
PATH: "${CIRRUS_WORKING_DIR}/otp/bin:${PATH}"
matrix:
- name: Linux, ${OTP_RELEASE}, Ubuntu 14.04
alias: Linux Stable
matrix:
- env:
CHECK_POSIX_COMPLIANT: true
CHECK_REPRODUCIBLE: true
OTP_RELEASE: OTP-23.0
- env:
OTP_RELEASE: OTP-22.3
- env:
OTP_RELEASE: OTP-22.0
- env:
OTP_RELEASE: OTP-21.3.8
- env:
OTP_RELEASE: OTP-21.0
- name: Linux, OTP-${OTP_RELEASE}, development, Ubuntu 14.04
alias: Linux Development
allow_failures: true
skip_notifications: true
depends_on:
- Linux Stable
- FreeBSD Stable
matrix:
- env:
OTP_RELEASE: master
- env:
OTP_RELEASE: maint
install_script:
- wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${OTP_RELEASE}.tar.gz
- mkdir -p otp
- tar zxf otp.tar.gz -C otp --strip-components=1
- otp/Install -minimal ${CIRRUS_WORKING_DIR}/otp
- rm -rf .git
- make compile
build_info_script: bin/elixir --version
test_formatted_script:
- make test_formatted &&
echo "All Elixir source code files are properly formatted."
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
test_erlang_script: make test_erlang
test_elixir_script: make test_elixir
check_posix_compliant_script: |
if [ -n "$CHECK_POSIX_COMPLIANT" ]; then
apt update
apt install -y shellcheck
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
else
echo "The format of the shell scripts is only checked in the last stable Erlang/OTP version."
fi
check_reproducible_script: |
if [ -n "$CHECK_REPRODUCIBLE" ]; then
make check_reproducible
else
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
fi
test_windows_task:
<<: *DEFAULT_TEST_SETTINGS
name: Windows, OTP-${OTP_RELEASE}, Windows Server 2019
alias: Windows Stable
matrix:
- env:
OS_VERSION: 2019
OTP_RELEASE: 22.0
- env:
OS_VERSION: 2019
OTP_RELEASE: 21.0.1
windows_container:
image: fertapric/elixir-ci:otp-win64-${OTP_RELEASE}
os_version: ${OS_VERSION}
cpu: 4
memory: 6GB
install_script:
- rmdir /s /q .git
- make compile
build_info_script: bin/elixir --version
test_formatted_script:
- make test_formatted &&
echo "All Elixir source code files are properly formatted."
test_erlang_script: make --keep-going test_erlang
test_elixir_script: make --keep-going test_elixir
test_freebsd_task:
<<: *DEFAULT_TEST_SETTINGS
-2
View File
@@ -1,3 +1 @@
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
*.ex diff=elixir
*.exs diff=elixir
-102
View File
@@ -1,102 +0,0 @@
name: CI
on: [pull_request, push]
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
jobs:
test_linux:
name: Linux, ${{ matrix.otp_release }}, Ubuntu 16.04
continue-on-error: ${{ matrix.development }}
strategy:
matrix:
otp_release: ['OTP-23.0', 'OTP-22.3', 'OTP-22.0', 'OTP-21.3.8', 'OTP-21.0']
development: [false]
include:
- otp_release: master
development: true
- otp_release: maint
development: true
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 50
- name: Install Erlang/OTP
run: |
cd $RUNNER_TEMP
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${{ matrix.otp_release }}.tar.gz
mkdir -p otp
tar zxf otp.tar.gz -C otp --strip-components=1
otp/Install -minimal $(pwd)/otp
echo "::add-path::$(pwd)/otp/bin"
- name: Compile Elixir
run: |
rm -rf .git
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Dyalizer
run: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
- name: Erlang test suite
run: make test_erlang
- name: Elixir test suite
run: make test_elixir
- name: Check reproducible builds
run: taskset 1 make check_reproducible
if: matrix.otp_release == 'OTP-23.0'
test_windows:
name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019
strategy:
matrix:
otp_release: ['22.0']
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@v1
with:
fetch-depth: 50
- name: Cache Erlang/OTP package
uses: actions/cache@v2
with:
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\erlang
key: OTP-${{ matrix.otp_release }}-windows-2019
- name: Install Erlang/OTP
run: choco install -y erlang --version ${{ matrix.otp_release }}
- name: Compile Elixir
run: |
remove-item '.git' -recurse -force
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make --keep-going test_erlang
- name: Elixir test suite
run: make --keep-going test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 50
- name: Install Shellcheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Check POSIX-compliant
run: |
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
+17
View File
@@ -0,0 +1,17 @@
on: check_suite
name: CI email
jobs:
sendEmail:
name: Send email
runs-on: ubuntu-latest
steps:
- name: Send email
# Source: https://github.com/elixir-lang/elixir-ci
uses: docker://fertapric/elixir-ci-email:latest
env:
APP_NAME: Cirrus CI
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAIL_FROM: ci@elixir-lang.org
MAIL_HOST: smtp.sendgrid.net
MAIL_PASSWORD: ${{ secrets.CI_EMAIL_PASSWORD }}
MAIL_USERNAME: ${{ secrets.CI_EMAIL_USERNAME }}
+312 -370
View File
@@ -1,449 +1,391 @@
# Changelog for Elixir v1.11
# Changelog for Elixir v1.10
Over the last releases, the Elixir team has been focusing on the compiler, both in terms of catching more mistakes at compilation time and making it faster. Elixir v1.11 has made excellent progress on both fronts. This release also includes many other goodies, such as tighter Erlang integration, support for more guard expressions, built-in datetime formatting, and other calendar enhancements.
## Support for Erlang/OTP 21+
## Tighter Erlang integration
Elixir v1.10 requires Erlang/OTP 21+, allowing Elixir to integrate with Erlang/OTP's new logger. Currently, this means that the logger level, logger metadata, as well as all log messages are now shared between Erlang and Elixir APIs.
Following Elixir v1.10, we have further integrated with Erlang's new logger by adding four new log levels: `notice`, `critical`, `alert`, and `emergency`, matching all log levels found in the Syslog standard. The `Logger` module now supports structured logging by passing maps and keyword lists to its various functions. It is also possible to specify the log level per module, via the `Logger.put_module_level/2` function. Log levels per application will be added in future releases.
We will continue improving the relationship between the logging systems in future releases. In particular, we plan to expose all log levels and runtime filtering functionalities available in Erlang directly into Elixir in the next Elixir version.
IEx also has been improved to show the documentation for Erlang modules directly from your Elixir terminal. This works with Erlang/OTP 23+ and requires Erlang modules to have been compiled with documentation chunks.
This release also adds two new guards, `is_struct/1` and `is_map_key/2`, thanks to the strict requirement on Erlang/OTP 21+.
## Compiler checks: application boundaries
## Releases improvements
Elixir v1.11 builds on top of the recently added compilation tracers to track application boundaries. From this release, Elixir will warn if you invoke a function from an existing module but this module does not belong to any of your listed dependencies.
Elixir v1.9 introduced releases as a mechanism to package self-contained applications. Elixir v1.10 further improves releases with bug fixes and new enhancements based on feedback we got from the community. The highlights are:
These two conditions may seem contradictory. After all, if a module is available, it must have come from a dependency. This is not true in two scenarios:
* Allow the dual boot system of releases to be disabled on environments that are boot-time sensitive, such as embedded devices
* Modules from Elixir and Erlang/OTP are always available - even if their applications are not listed as a dependency
* Track and raise if compile-time configuration is set or changes at runtime (more in the next section)
* In an umbrella project, because all child applications are compiled within the same VM, you may have a module from a sibling project available, even if you don't depend on said sibling
* Support for easily adding extra files to releases via overlays
This new compiler check makes sure that all modules that you invoke are listed as part of your dependencies, emitting a warning like below otherwise:
* Allow `RELEASE_DISTRIBUTION` to be set to `none` in order to fully disable it
```text
:ssl.connect/2 defined in application :ssl is used by the current
application but the current application does not directly depend
on :ssl. To fix this, you must do one of:
* Add a built-in `:tar` step that automatically packages releases
1. If :ssl is part of Erlang/Elixir, you must include it under
:extra_applications inside "def application" in your mix.exs
See the full CHANGELOG for more improvements.
2. If :ssl is a dependency, make sure it is listed under "def deps"
in your mix.exs
## Improvements to sort-based APIs in Enum
3. In case you don't want to add a requirement to :ssl, you may
optionally skip this warning by adding [xref: [exclude: :ssl]
to your "def project" in mix.exs
```
This comes with extra benefits in umbrella projects, as it requires applications to depend on the siblings they depend on, which will fail if there are any cyclic dependencies.
## Compiler checks: data constructors
In Elixir v1.11, the compiler also tracks structs and maps fields across a function body. For example, imagine you wanted to write this code:
def drive?(%User{age: age}), do: age >= 18
If there is either a typo on the `:age` field or the `:age` field was not yet defined, the compiler will fail accordingly. However, if you wrote this code:
def drive?(%User{} = user), do: user.age >= 18
The compiler would not catch the missing field and an error would only be raised at runtime. With v1.11, Elixir will track the usage of all maps and struct fields within the same function, emitting warnings for cases like above:
```text
warning: undefined field `age` in expression:
# example.exs:7
user.age
expected one of the following fields: name, address
where "user" was given the type %User{} in:
# example.exs:7
%User{} = user
Conflict found at
example.exs:7: Check.drive?/1
```
The compiler also checks binary constructors. Consider you have to send a string over the wire with length-based encoding, where the string is prefixed by its length, up to 4MBs. Your initial attempt may be this:
def run_length(string) when is_binary(string) do
<<byte_size(string)::32, string>>
end
However, the code above has a bug. Each segment given between `<<>>` must be an integer, unless specified otherwise. With Elixir v1.11, the compiler will let you know so:
```text
warning: incompatible types:
binary() !~ integer()
in expression:
<<byte_size(string)::integer()-size(32), string>>
where "string" was given the type integer() in:
# foo.exs:4
<<byte_size(string)::integer()-size(32), string>>
where "string" was given the type binary() in:
# foo.exs:3
is_binary(string)
HINT: all expressions given to binaries are assumed to be of type integer()
unless said otherwise. For example, <<expr>> assumes "expr" is an integer.
Pass a modifier, such as <<expr::float>> or <<expr::binary>>, to change the
default behaviour.
Conflict found at
foo.exs:4: Check.run_length/1
```
Which can be fixed by adding `::binary` to the second component:
def run_length(string) when is_binary(string) do
<<byte_size(string)::32, string::binary>>
end
While some of those warnings could be automatically fixed by the compiler, future versions will also perform those checks across functions and potentially across modules, where automatic fixes wouldn't be desired (nor possible).
## Compilation time improvements
Elixir v1.11 features many improvements to how the compiler tracks file dependencies, such that touching one file causes less files to be recompiled. In previous versions, Elixir tracked three types of dependencies:
* compile time dependencies - if A depends on B at compile time, such as by using a macro, whenever B changes, A is recompiled
* struct dependencies - if A depends on B's struct, whenever B's struct definition changed, A is recompiled
* runtime dependencies - if A depends on B at runtime, A is never recompiled
However, because dependencies are transitive, if A depends on B at compile time and B depends on C at runtime, A would depend on C at compile time. Therefore, it is very important to reduce the amount of compile time dependencies.
Elixir v1.11 replaces "struct dependencies" by "exports dependencies". In other words, if A depends on B, whenever B public's interface changes, A is recompiled. B's public interface is made by its struct definition and all of its public functions and macros.
This change allows us to mark `import`s and `require`s as "exports dependencies" instead of "compile time" dependencies. This simplifies the dependency graph considerably. For example, [in the Hex.pm project](https://github.com/hexpm/hexpm), changing the `user.ex` file in Elixir v1.10 would emit this:
```text
$ touch lib/hexpm/accounts/user.ex && mix compile
Compiling 90 files (.ex)
```
In Elixir v1.11, we now get:
```text
$ touch lib/hexpm/accounts/user.ex && mix compile
Compiling 16 files (.ex)
```
To make things even better, Elixir v1.11 also introduces a more granular file tracking for path dependencies. In previous versions, a module from a path dependency would always be treated as a compile time dependency. This often meant that if you have an umbrella project, changing an application would cause many modules in sibling applications to recompile. Fortunately, Elixir v1.11 will tag modules from dependencies as exports if appropriate, yielding dramatic improvements to those using path dependencies.
To round up the list of compiler enhancements, the `--profile=time` option added in Elixir v1.10 now also includes the time to compile each individual file. For example, in the Plug project, one can now get:
```text
[profile] lib/plug/conn.ex compiled in 935ms
[profile] lib/plug/ssl.ex compiled in 147ms (plus 744ms waiting)
[profile] lib/plug/static.ex compiled in 238ms (plus 654ms waiting)
[profile] lib/plug/csrf_protection.ex compiled in 237ms (plus 790ms waiting)
[profile] lib/plug/debugger.ex compiled in 719ms (plus 947ms waiting)
[profile] Finished compilation cycle of 60 modules in 1802ms
[profile] Finished group pass check of 60 modules in 75ms
```
While implementing those features, we have also made the `--long-compilation-threshold` flag more precise. In previous versions, `--long-compilation-threshold` would consider both the time a file spent to compile and the time spent waiting on other files. In Elixir v1.11, it considers only the compilation time. This means less false positives and you can now effectively get all files that take longer than 2s to compile by passing `--long-compilation-threshold 2`.
## `mix xref graph` improvements
To bring visibility to the compiler tracking improvements described in the previous section, we have also added new features to `mix xref`. `mix xref` is a task that describes cross-references between files in your projects. The `mix xref graph` subsection focuses on the dependency graph between them.
First we have made the existing `--label` flag to consider transitive dependencies. Using `--sink FILE` and `--label compile` can be a powerful combo to find out which files will change whenever the given `FILE` changes. For example, in the Hex.pm project, we get:
```text
$ mix xref graph --sink lib/hexpm/accounts/user.ex --label compile
lib/hexpm/billing/hexpm.ex
└── lib/hexpm/billing/billing.ex (compile)
lib/hexpm/billing/local.ex
└── lib/hexpm/billing/billing.ex (compile)
lib/hexpm/emails/bamboo.ex
├── lib/hexpm/accounts/email.ex (compile)
└── lib/hexpm/accounts/user.ex (compile)
lib/hexpm/emails/emails.ex
└── lib/hexpm_web/views/email_view.ex (compile)
lib/hexpm_web/controllers/api/docs_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/key_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/organization_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/organization_user_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/owner_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/package_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/release_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/repository_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/retirement_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/blog_controller.ex
└── lib/hexpm_web/views/blog_view.ex (compile)
lib/hexpm_web/endpoint.ex
├── lib/hexpm_web/plug_parser.ex (compile)
└── lib/hexpm_web/session.ex (compile)
```
All the files at the root will recompile if `lib/hexpm/accounts/user.ex` changes. Their children describe the *why*. For example, the `repository_controller.ex` file will recompile if user changes because it has a compile time dependency on `auth_helpers.ex`, which depends on `user.ex`. This indirect compile time dependency is often the source of recompilations and Elixir v1.11 now makes it trivial to spot them, so they can be eventually addressed.
Another improvement to `mix xref graph` is the addition of `--format cycles`, which will print all cycles in your compilation dependency graph. A `--min-cycle-size` flag can be used if you want to discard short cycles.
## `config/runtime.exs` and `mix app.config`
Elixir v1.9 introduced a new configuration file, specific to releases, called `config/releases.exs`. A release is a self-contained artifact with the Erlang VM, Elixir and your application, ready to run in production.
The addition of `config/releases.exs` has been a very useful one but, unfortunately, it applies only to releases. Developers not using releases must use the `config/config.exs` file, which often loaded too early at compilation time. For any dynamic configuration, developers had to resort to third-party tools or workarounds to achieve the desired results.
Elixir v1.11 addresses this issue by introducing a new configuration file, called `config/runtime.exs`. This new configuration file is loaded exactly before your application starts, when the code is already fully compiled. It is loaded in development, test, and production, regardless if you are using Mix or releases. Therefore it provides a unified API for runtime configuration in Elixir.
`config/runtime.exs` works the same as any other configuration file. However, given `config/runtime.exs` is meant to run with or without Mix, developers must not use `Mix.env()` or `Mix.target()` in `config/runtime.exs`. Instead, they must use the new `config_env()` and `config_target()`, which have been added to the `Config` module.
While `config/releases.exs` will continue to be supported, developers can migrate to `config/runtime.exs` without loss of functionality. For example, a `config/releases.exs` file such as this one
`Enum.sort/1` in Elixir by default sorts from lowest to highest:
```elixir
# config/releases.exs
import Config
config :foo, ...
config :bar, ...
iex> Enum.sort(["banana", "apple", "pineapple"])
["apple", "banana", "pineapple"]
```
could run as is as `config/runtime.exs`. However, given `config/runtime.exs` runs in all environments, you may want to restrict part of your configuration to the `:prod` environment:
If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code:
```elixir
# config/runtime.exs
import Config
iex> Enum.sort(["banana", "apple", "pineapple"], &>=/2)
["pineapple", "banana", "apple"]
```
if config_env() == :prod do
config :foo, ...
config :bar, ...
Furthermore, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result:
```elixir
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]])
[~D[2020-01-01], ~D[2019-12-31]]
```
To perform proper semantic comparison for dates, one would also need to pass a custom sorting function:
```elixir
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], &(Date.compare(&1, &2) != :lt))
[~D[2019-12-31], ~D[2020-01-01]]
```
Elixir v1.10 streamlines the sorting functions by introducing both `:asc` and `:desc` shortcuts:
```elixir
iex> Enum.sort(["banana", "apple", "pineapple"], :asc)
["apple", "banana", "pineapple"]
iex> Enum.sort(["banana", "apple", "pineapple"], :desc)
["pineapple", "banana", "apple"]
```
As well as adding the possibility to pass a module to perform semantic comparisons. For example, to sort dates, one now only needs to pass the `Date` module or even `{:desc, Date}` for descending semantical sort:
```elixir
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], Date)
[~D[2019-12-31], ~D[2020-01-01]]
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], {:desc, Date})
[~D[2020-01-01], ~D[2019-12-31]]
```
These API improvements make the code more concise and readable and they have also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
## Tracking of compile-time configuration
In Elixir, we organize our code in applications. Libraries, your dependencies, and your own project are all separate applications. All applications in Elixir also come with an application environment.
The application environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
```elixir
defmodule MyApp.DBClient do
@db_host Application.get_env(:my_app, :db_host, "db.local")
def start_link() do
SomeLib.DBClient.start_link(host: @db_host)
end
end
```
If both files are available, releases will pick the now preferred `config/runtime.exs` instead of `config/releases.exs`.
This approach has one big limitation: if you change the value of the application environment after the code is compiled, the value used at runtime is not going to change! For example, if you are using `mix release` and your `config/releases.exs` has:
To wrap it all up, `Mix` also includes a new task called `mix app.config`. This task loads all applications and configures them, without starting them. Whenever you write your own Mix tasks, you will typically want to invoke either `mix app.start` or `mix app.config` before running your own code. Which one is better depends if you want your applications running or only configured.
config :my_app, :db_host, "db.production"
## Other improvements
Because `config/releases.exs` is read after the code is compiled, the new value will have no effect as the code was compiled to connect to "db.local".
Elixir v1.11 adds the `is_struct/2`, `is_exception/1`, and `is_exception/2` guards. It also adds support for the `map.field` syntax in guards.
Of course, the obvious solution to this mismatch is to not read the application environment at compilation time in the first place, and instead move the code to inside a function:
The Calendar module ships with a new `Calendar.strftime/3` function, which provides datetime formatting based on the `strftime` format. The `Date` module got new functions for working with weeks and months, such as `Date.beginning_of_month/1` and `Date.end_of_week/2`. Finally, all calendar types got conversion functions from and to gregorian timestamps, such as `Date.from_gregorian_days/2` and `NaiveDateTime.to_gregorian_seconds/1`.
```elixir
defmodule MyApp.DBClient do
def start_link() do
SomeLib.DBClient.start_link(host: db_host())
end
defp db_host() do
Application.get_env(:my_app, :db_host, "db.local")
end
end
```
Mix also includes two new tasks: `mix app.config`, for application runtime configuration, and `mix test.coverage`, which generates aggregated coverage reports for umbrella projects and for test suites partitioned across processes.
While this is the preferred approach, there are still two scenarios we need to address:
## v1.11.2 (2020-11-03)
1. Not everyone may be aware of this pitfall, so they will mistakenly read the application environemnt at compile-time, until they are bitten by this behaviour
2. In rare occasions, you trully need to read the application environment at compile-time, and you want to be warned when you try to configure at runtime something that is valid only at compilation time
Elixir v1.10 aims to solve these two scenarios by introducing a `Application.compile_env/3` function. For example, to read the value at compile time, you can now do:
```elixir
@db_host Application.compile_env(:my_app, :db_host, "db.local")
```
By using `compile_env/3`, Elixir will store the values used during compilation and compare them with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
In future versions, we will deprecate the use `Application.get_env` at compile-time with a clear message pointing users to configuration best practices, effectively addressing the scenario where users read from the application environment at compile time unaware of its pitfalls.
## Compiler tracing
This release brings enhancements to the Elixir compiler and adds new capabilities for developers to listen to compilation events.
In previous Elixir versions, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project in order to perform all kinds of checks, such as deprecations and undefined functions.
Although this database was not public, developers would still use it to run their own checks against their projects. With time, developers would request more data to be included in the database, which was problematic as Elixir itself did not have a use for the additional data, and the database was not meant to be used externally in the first place.
In Elixir v1.10, we have addressed these problems by introducing compiler tracing. The compiler tracing allows developers to listen to events as they are emitted by the compiler, so they can store all of the information they need - and only the information they need.
Elixir itself is using the new compiler tracing to provide new functionality. One advantage of this approach is that developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
@compile {:no_warn_undefined, OptionalDependency}
defdelegate my_function_call(arg), to: OptionalDependency
Previously, this information had to be added to the overall project configuration, which was far away from where the optional call effectively happened.
## Other enhancements
Elixir's calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
There are many improvements related to Elixir's AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information was already available to the Elixir code formatter and has now been made public. We have also extensively documented all of Elixir's AST metadata. These changes alongside compiler tracing means static analyzers and IDE integrations have a better foundation to analyze the source code.
ExUnit, our test framework, ships two small but important improvements: `ExUnit.CaptureIO` can now be used by tests that run concurrently and we have added "pattern-matching diffing". To understand the last feature, take this code:
```elixir
assert %{"status" => 200, "body" => %{"key" => "foo"}} = json_payload
```
Now imagine that `json_payload` is a large JSON blob and the `"key"` inside the `"body"` did not have value of `"foo"`. In previous Elixir versions, if the assertion failed, Elixir would print the right side and let you up to your own devices to figure out what went wrong. In Elixir v1.10, we diff the data structure against the pattern so you can see exactly which parts of the data matched the pattern and which ones did not. Note ExUnit already performed diffing when comparing data types, this new version adds diffing when matching data against a pattern.
## v1.10.4 (2020-07-04)
### 1. Bug fixes
#### Elixir
* [Code] Do not crash when getting docs for missing `erts` appdir
* [Kernel] Raise meaningful error if `:erlang.is_record` is used in guards
* [Kernel] Prune tracers when fetching `__ENV__` inside functions
#### Mix
* [mix] Fix regression where aliases could not call themselves recursively
* [mix compile] Do not discard tracers that are set programatically
## v1.11.1 (2020-10-16)
### 1. Bug fixes
#### Elixir
* [Code] Ignore tracers if lexical tracker is dead or explicitly nil when evaling code with an environment
* [GenServer] Do not show warning when using `super` in `GenServer.child_spec/1`
* [Kernel] Do not crash when :reduce is set to `nil` in comprehensions
* [Kernel] Fix a scenario where undefined function warnings were not being emitted
#### IEx
* [IEx.Helpers] Properly handle tags inside typespec when showing Erlang docs
#### Logger
* [Logger] Do not deadlock Logger if handler crashes on sync mode
#### Mix
* [Mix] Add inet6 fallback to Mix usage of httpc
* [mix compile.app] Do not list apps that do not match the current target
## v1.11.0 (2020-10-06)
### 1. Enhancements
#### EEx
* [EEx] Track column information in EEx templates when enabled in the compiler
* [EEx] Show column information in EEx error messages
* [EEx] Support `:indentation` option when compiling EEx templates for proper column tracking
#### Elixir
* [Access] Add `Access.at!/1`
* [Calendar] Add `Calendar.strftime/3` for datetime formatting
* [Calendar] Add linear integer representations to Calendar modules: `Date.from_gregorian_days/2`, `Date.to_gregorian_days/1`, `NaiveDateTime.from_gregorian_seconds/3`, `NaiveDateTime.to_gregorian_seconds/1`, `Time.from_seconds_after_midnight/1`, and `Time.to_seconds_after_midnight/1`
* [Calendar] Add `new!` to Date/Time/NaiveDateTime/DateTime (`new` has also been added to `DateTime` for completeness)
* [Calendar] Support custom starting day of the week in `Date.day_of_week/2`
* [Calendar] Add `Date.beginning_of_month/1` and `Date.end_of_month/1`
* [Calendar] Add `Date.beginning_of_week/2` and `Date.end_of_week/2`
* [Code] Add `:column` to `Code.string_to_quoted*/2`
* [Code] Add `Code.can_await_module_compilation?/0` to check if the parallel compiler is enabled and it can await for other modules to be compiled
* [Config] Support `config_env/0` and `config_target/0` in `config` files
* [Config] Allow `import_config` to be disabled for some configuration files
* [Enum] Allow a sorting function on `Enum.min_max_by/3,4`, including the new `compare/2` conventions
* [Kernel] Add `is_struct/2` guard
* [Kernel] Add `is_exception/1` and `is_exception/2` guards
* [Kernel] Support `map.field` syntax in guards
* [Kernel] Add `+++` and `---` with right associativity to the list of custom operators
* [Kernel] Warn if a variable that looks like a compiler variable (such as `__MODULE__`) is unused
* [Kernel.ParallelCompiler] Report individual file compilation times when `profile: :time` is given
* [Kernel.ParallelCompiler] Improve precision of `:long_compilation_threshold` so it takes only compilation times into account (and not waiting times)
* [Registry] Add `Registry.delete_meta/2`
* [Task] Add `Task.await_many/2`
* [Kernel] Fix a bug where custom types were printed as built-in types
* [Kernel] Don't add compile-time dependency on `defdelegate`
* [Kernel] Add line numbers to warnings on deprecated imports
* [Kernel] Report the correct line number when raising inside a macro
* [Task] Include callers in translated Logger metadata for Task
* [Task] Fix Task PID and caller in Task Supervisor reports
#### ExUnit
* [ExUnit] Add support for coloring on Windows 10 consoles/shells
* [ExUnit] Add `ExUnit.fetch_test_supervisor/0`
* [ExUnit] Add `@tag :tmp_dir` support to ExUnit. The temporary directory is automatically created and pruned before each test
* [ExUnit] Add file and line to ExUnit's `--trace`
* [ExUnit.Assertion] Allow receive timeouts to be computed at runtime
* [ExUnit.Case] Add `register_test/6` to speed up compilation of custom tests
* [ExUnit.Doctest] Allow users to add tags to doctests
* [ExUnit.Formatter] Avoid crashes when diffing guards when the pattern does not match
* [ExUnit.Formatter] Also blame exceptions that come from linked and trapped exits
#### IEx
* [IEx] Add support for coloring on Windows 10 consoles/shells
* [IEx.Helpers] Show docs from Erlang modules that have been compiled with the docs chunk
* [IEx.Helpers] Do not crash when printing a type that cannot be code formatted
#### Logger
#### Mix
* [Logger] Add `notice`, `critical`, `alert`, and `emergency` log levels
* [Logger] Support structured logging by logging maps or keyword lists
* [Logger] Allow level to be set per module with `Logger.put_module_level/2`
* [Logger] Include `erl_level` in Logger's metadata
* [mix app.start] Fix reading `.app` file located in archives (`.ez` files)
* [mix local.hex] Provide more guidance when Hex can't be installed
* [mix release] Properly encode config in releases
## v1.10.3 (2020-04-25)
### 1. Bug fixes
#### Elixir
* [Code] Return `[{mod, bin}]` from `Code.compile_file/2`, `Code.require_file/2`, `Code.load_file/2`
* [Code] Make sure the formatter respects newlines before and after module attributes
* [Kernel.ParallelCompiler] Fix a bug where the parallel compiler would raise in long compilation cycles
* [Kernel.ParallelCompiler] Fix a bug where the parallel compiler would raise if some of the modules being compiled referred to a module that has been loaded directly to memory
* [Module] Fix accidental breaking change where bodiless clauses had their body value on `@on_definition` callbacks set to an empty list instead of `nil`
* [String] Undeprecate `String.normalize/2` normalize and fix infinite loop caused by certain invalid strings
#### ExUnit
* [ExUnit.Assertions] Fix pattern matching diff when matching on pinned variables
* [ExUnit.Assertions] Fix pattern matching diff when matching variable struct names
* [ExUnit.Assertions] Fix pattern matching diff when matching on the binary concat operator (`<>`) and the left side is not a literal string
* [ExUnit.Assertions] Fix pattern matching diff when matching on pseudo-vars (`__MODULE__`, `__DIR__`, etc)
#### Mix
* [mix] Add `MIX_BUILD_ROOT` to config `_build` dir
* [mix] Introduce `MIX_XDG` as a simpler mechanism to opt-in to the XDG specification
* [mix] Allow requirements for a Mix task to be listed via the `@requirements` module attribute
* [mix] Allow optional dependencies to be defined in `:extra_applications` and `:applications`
* [mix app.config] Add new `mix app.config` task that compiles applications and loads runtime configuration
* [mix archive.install] Support `--repo` option on Hex packages
* [mix compile] Support the `__mix_recompile__?/0` callback for custom behaviour on when Mix should recompile a given module
* [mix compile.elixir] Mark modules for path dependencies as "Export dependencies" if they changed but their public interface is the same
* [mix compile.elixir] Track application boundaries in the Elixir compiler. If you invoke code from Erlang or Elixir standard libraries and you don't depend on the proper applications, a warning will be emitted. A warning will also be emitted if you invoke code from an umbrella sibling that you don't depend on - effectively forbidding cyclic dependencies between apps
* [mix deps] Sort the dependencies alphabetically before printing
* [mix deps] Use `origin/HEAD` as the default Git ref in dependencies
* [mix deps] Redact Git `username`/`password` in output log
* [mix deps] Support rebar3's `git_subdir` resource type
* [mix deps.compile] Allow local deps to be skipped on `mix deps.compile`
* [mix deps.unlock] Print which dependencies get unlocked when using the `--unused` flag
* [mix escript.install] Support `--repo` option on Hex packages
* [mix new] Add `@impl` to application generated by `mix new --sup`
* [mix release] Enable overriding `sys.config` location via `RELEASE_SYS_CONFIG` env var
* [mix release] Boot a release under configuration in interactive mode and then swap to embedded mode (if running on Erlang/OTP 23+)
* [mix release] Add `rel_templates_path` to configure the source of template files such as "env.sh.eex", "vm.args.eex" and "overlays"
* [mix release] Allow some chunks to be kept in the `:strip_beams` config
* [mix test] Allow `:ignore_modules` inside `:test_coverage` option
* [mix test.coverage] Add `mix test.coverage` that aggregates coverage results from umbrellas and OS partitioning
* [mix xref] Make the `--label` option for `mix xref graph` transitive by default and add `--only-direct` for only direct dependencies
* [mix xref] Add `--format cycles` support for `mix xref graph`
* [mix xref] Add support to `mix xref graph` for using `--source` and `--sink` at the same time
* [mix release] Respect the `:path` option when creating a `:tar` file for releases
## v1.10.2 (2020-02-26)
### 1. Bug fixes
#### Elixir
* [Macro] Fix a bug where `Macro.to_string/1` would emit invalid code for sigils
* [Task] Do not crash `async_stream` monitor if it receives spurious DOWN messages
#### Logger
* [Logger] Fix a bug where the Logger formatter would fail when handling unknown metadata values
#### Mix
* [mix compile] Do not write files to disk if `--warnings-as-errors` was given and warnings were emitted
## v1.10.1 (2020-02-10)
### 1. Bug fixes
#### Elixir
* [Code] Do not emit invalid code when formatting `nil`, `false`, and `true` keys in maps
* [Kernel] Ensure `with` clauses properly unpack "implicit guards" (such as matching on the struct name)
* [Kernel] Do not warn if commas are used by themselves in `~w`/`~W` sigils
* [Kernel] Do not validate the `:line` option in quote (the validation has been moved to v1.11 to give users more time to update their code)
* [Module] Ensure the code verifier handles the `:erlang.size/1` guard properly
#### Logger
* [Logger] Properly handle the `report_cb/2` option from Erlang
* [Logger] Fix truncation for multi-byte characters
* [Logger] Do not rebroadcast messages from remote nodes as this is now taken care by Erlang's logger
#### ExUnit
* [ExUnit] Ensure `assert_receive` produces valid exception messages in case of errors
#### Mix
* [mix release] Make sure the install command (Window specific) works on paths with spaces in the name
* [mix release] Allow using `remote` and `rpc` commands with `Application.compile_env/3`
## v1.10.0 (2020-01-27)
### 1. Enhancements
#### Elixir
* [Application] Add `Application.compile_env/3` and `Application.compile_env!/2` for reading values at compilation time and tracking if they accidentally change during runtime
* [Calendar] Allow custom calendar representations in calendar sigils
* [Calendar] Add `c:Calendar.parse_time/1`, `c:Calendar.parse_date/1`, `c:Calendar.parse_naive_datetime/1` and `c:Calendar.parse_utc_datetime/1` callbacks to calendar behaviour
* [CLI] Add support for `NO_COLOR` environment variable
* [Code] Add `:token_metadata` and `:literal_encoder` support to `Code.string_to_quoted/2`
* [Code] Add compiler tracing to lift events done by the compiler
* [Code] Return `{:error, :unavailable}` in `Code.ensure_compiled/1` if module is in a deadlock
* [DateTime] Add `DateTime.now!/2` and `DateTime.shift_zone!/3`
* [Enum] Speed up getting one random element from enumerables
* [Enum] Add `Enum.frequencies/1`, `Enum.frequencies_by/2`, and `Enum.map_intersperse/2`
* [Enum] Allow a sorting function on `Enum.min/max/min_by/max_by`
* [Enum] Add `asc/desc` and `compare/1` support to `Enum.sort/2`
* [Exception] Add version alongside app names in stacktraces
* [Function] Add `Function.identity/1`
* [Kernel] Add `Kernel.is_struct/1` and `Kernel.is_map_key/2`
* [Kernel] Warn when function head comes immediately after the implementation instead of before the implementation
* [Kernel] Warn if duplicate key is found in struct declaration
* [Kernel] Print all undefined functions as warnings and then raise. This allows users to see all undefined calls at once, when it would otherwise require them to compile the code multiple times
* [Kernel] Allow file, line and context to be dynamically set on `quote`
* [Keyword] Add `Keyword.pop!/2` and `Keyword.pop_values/2`
* [Map] Add `Map.pop!/2`
* [MapSet] Optimize multiple operations
* [Module] Add `Module.has_attribute?/2`
* [Module] Add `@compile {:no_warn_undefined, mfa_or_module}` to turn off undefined function warnings
* [NaiveDateTime] Add `NaiveDateTime.local_now/0`
* [Record] Warn if duplicate key is found in record declaration
* [String] Update to Unicode 12.1
* [StringIO] Add `:encoding` option to StringIO and optimize `get_chars` operation
#### ExUnit
* [ExUnit.Assertions] Support diffs in pattern matching and in `assert_receive`
* [ExUnit.CaptureIO] Supports capturing named devices in asynchronous tests
#### IEx
* [IEx] Warn on circular file imports when loading default `.iex.exs`
* [IEx] Allow customization of the continuation prompt on IEx
#### Logger
* [Logger] Allow `start_options` to be configured on Logger's GenEvent
* [Logger] Integrate Elixir's Logger with Erlang/OTP 21+'s logger. This means setting up the logger level in Elixir will automatically change the logger level for Erlang and vice-versa
#### Mix
* [mix compile] Add `--profile time` flag to profile compilation steps
* [mix deps.compile] Add `--skip-umbrella-children` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
* [mix deps.unlock] Add `--check-unused` flag. The new flag raises if there are any unused dependencies in the lock file
* [mix release] Allow `RELEASE_DISTRIBUTION` to be set to `none`
* [mix release] Support overlays in `rel/overlays`
* [mix release] Allow configuration reboot to be disabled in releases
* [mix test] Add support for simple round-robin test partitioning across multiple machines
* [Mix.Project] Add `MIX_DEPS_PATH` environment variable for setting `:deps_path`
* [Mix.Project] Add `Mix.Project.deps_scms/1` that returns deps with their SCMs
* [Mix.Task] Add `Mix.Task.Compiler.after_compiler/2` callback, to simplify compilers that may need to run something at multiple steps
### 2. Bug fixes
#### EEx
* [EEx] Make trimming behaviour via the `:trim` option more consistent
* [EEx] Ensure multiline do/end with no spaces compile under trim mode
#### Elixir
* [Application] Warn if non-atom keys are given to `put_env`, `get_env`, `fetch_env`, and `delete_env`
* [Code] Do not send language keyword through the `:static_atoms_encoder` in `Code.string_to_quoted`
* [Kernel] Validate values given to `:line` in quote to avoid emitting invalid ASTs
* [Kernel] Report the correct line number when raising inside a macro
* [Kernel] Fix an issue where `elixirc` would not accept paths with backslash (`\`) separators on Windows
* [Kernel] Properly parse `&//2` (i.e. the capture of the division operator)
* [Kernel] Raise `CompileError` when trying to define reserved types
* [Kernel] Improve compiler error message when using `|` in a `def` signature
* [Kernel] Improve error message when trying to use invalid list operators in guards
* [Kernel.SpecialForms] Add `|/2` to the list of special forms to avoid inconsistent behaviour on overrides
* [Keyword] Enforce keys to be atoms in `Keyword.keys/1`
* [Record] Keep lexical ordering when creating records
* [Registry] Do not crash when a process with key-value has been registered using `:via` and it fails to start on `init`
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to [URL's living standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded) - note this behaviour is not specified in the spec implemented by the URI module, so the living standard was chosen
* [Version] Add defaults and enforce keys in `Version` struct
* [Enum] Allow positive range slices on infinite streams given to `Enum.slice/2`
* [Kernel] Raise error on functions/guards without implementation
* [Kernel] Do not expand expressions inside interpolation twice
* [Keyword] Ensure keyword replace and update preserve order
* [Module] Raise instead of silently failing when performing a write module operation during after-compile
* [Module] Fix `@macrocallback` definitions with a `when` clause
* [Path] Fix `Path.absname/1` to correctly handle UNC paths on Windows
* [Stream] Close with correct accumulator in `Stream.resource/3` when called for a single-element list
* [Stream] Allow `Stream.cycle/1` to be double nested inside `Stream.cycle/1`
* [URI] Preserve slashes in URIs without authority
* [URI] Require a nil or an absolute path on URIs with host or authority
#### ExUnit
* [ExUnit.CaptureIO] Fix race condition where a dead capture would still be considered as active
* [ExUnit.Diff] Do not crash when failing to eval/inspect struct
* [ExUnit.Diff] Properly diff numbers in respect to `==` and `===` operators
* [ExUnit.Assertions] Fix `assert_receive` and `assert match?` to behave consistently compared `receive` and `match?` when given an invalid macro
#### IEx
* [IEx] Fix tokenizer emitting repeated warnings in the REPL
* [IEx] Ensure `--dot-iex` is preserved when restarting the evaluator and after shell respawn
* [IEx.Pry] Ensure `IEx.pry` can be triggered more than twice when invoked from the same process
* [IEx] Exit IEx session if the group leader exits
* [IEx] Allow `pry` to be used in non-tty terminals
#### Mix
* [mix cmd] Fix a bug where only the first --app option would be executed
* [mix compile] Fix an issue where new protocol implementations would not propagate when running `mix compile` from an umbrella root
* [mix deps.compile] Use `gmake` instead of `make` when compiling deps on NetBSD/DragonFlyBSD
* [mix release] Load `.app` from dependencies path when it is a project dependency
* [mix release] Always include "rel/overlays" in the list of overlays directories if available
* [mix release] Change `erts/bin/erl` binary mode to `0o755`
* [mix test] Compare to test coverage threshold inclusively
#### Logger
* [Logger] Print metadata for all types that implement String.Chars
* [mix compile] Do not filter out warning for external files from diagnostics
* [Mix.Project] Ensure user given `:manager` to dependencies has higher precedence than the SCM one
* [Mix.Project] Recompile umbrella children when config files change and `mix compile` is called from the umbrella root
* [Mix.Task] Always recompile before running tasks from dependencies
* [Mix.Task] Ensure project's Logger config is used when running Mix tasks
### 3. Soft-deprecations (no warnings emitted)
### Elixir
#### Elixir
* [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1`
* [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2`
* [Code] `compiler_options/0` is deprecated in favor of `compiler_option/1`
### Logger
#### Mix
* [Logger] `warn` log level is deprecated in favor of `warning`
### Mix
* [mix release] `config/releases.exs` is deprecated in favor of a more general purpose `config/runtime.exs`
* [Mix.Config] `Mix.Config.persist/1` has been deprecated. Instead of `Mix.Config.persist(config)` use `Application.put_all_env(config, persistent: true)` (`Application.put_all_env/2` was added in v1.9)
* [mix xref] `calls/0` is deprecated in favor of compiler tracer
* [mix xref] The `xref.exclude` option has been moved to `elixirc_options.no_warn_undefined` as the `xref` pass has been moved into the compiler
### 4. Hard-deprecations
#### Elixir
* [Supervisor] Deprecate `Supervisor.start_child/2` and `Supervisor.terminate_child/2` in favor of `DynamicSupervisor`
* [Supervisor.Spec] Deprecate `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` in favor of the new typespecs
* [System] Deprecate `System.stacktrace/0` in favor of `__STACKTRACE__`
* [Code] `Code.load_file/2` has been deprecated in favor of `Code.require_file/2` or `Code.compile_file/2`
* [Code] `Code.loaded_files/0` and `Code.unload_file/1` have been deprecated in favor of `Code.required_files/0` and `Code.unrequire_file/1` respectively
* [Code] `Code.ensure_compiled?/1` is deprecated in favor of `Code.ensure_compiled/1`
* [String] `String.normalize/2` has been deprecated in favor of `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` which ship as part of Erlang/OTP 20+
* [Supervisor] `Supervisor.Spec.supervise/2` has been deprecated in favor of the new Supervisor child specification
* [Supervisor] The `:simple_one_for_one` strategy in `Supervisor` has been deprecated in favor of `DynamicSupervisor`
#### Logger
* [Logger] `:compile_time_purge_level` application environment configuration has been deprecated in favor of the more general `:compile_time_purge_matching` config
* [Logger] Deprecate logging non-chardata values
#### Mix
* [Mix.Project] Deprecate `Mix.Project.compile/2` in favor of `Mix.Task.run("compile", args)`
* [mix compile.xref] This check has been moved into the compiler and has no effect now
* [mix xref] `xref` now only tracks dependencies between modules and files, no longer between functions. See "Compilation tracers" to learn more about how to track this information directly
* [mix xref deprecations] This check has been moved into the compiler and has no effect now
* [mix xref unreachable] This check has been moved into the compiler and has no effect now
## v1.10
## v1.9
The CHANGELOG for v1.10 releases can be found [in the v1.10 branch](https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md).
The CHANGELOG for v1.9 releases can be found [in the v1.9 branch](https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md).
+11 -12
View File
@@ -2,10 +2,9 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := v1.11/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
CANONICAL := v1.10/ # master/ or vMAJOR.MINOR/
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include $(ERLC_OPTS)
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
@@ -75,7 +74,7 @@ compile: erlang $(APP) elixir
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
$(Q) cd lib/elixir && mkdir -p ebin && erl -make
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -146,12 +145,12 @@ check_reproducible: compile
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) $(MAKE) compile
$(Q) echo "Diffing..."
$(Q) bin/elixir lib/elixir/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) diff -r lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
$(Q) diff -r lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
$(Q) diff -r lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
$(Q) diff -r lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
$(Q) diff -r lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
$(Q) diff -r lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible"
clean:
@@ -295,7 +294,7 @@ PLT = .elixir.plt
$(PLT):
@ echo "==> Building PLT with Elixir's dependencies..."
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets crypto runtime_tools ftp tftp mnesia public_key asn1 hipe sasl
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets
clean_plt:
$(Q) rm -f $(PLT)
+3 -3
View File
@@ -1,6 +1,6 @@
# ![Elixir](https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png)
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=master)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amaster+workflow%3ACI) [![Build status](https://api.cirrus-ci.com/github/elixir-lang/elixir.svg?branch=master)](https://cirrus-ci.com/github/elixir-lang/elixir)
![Elixir](https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png)
=========
[![Build status](https://api.cirrus-ci.com/github/elixir-lang/elixir.svg?branch=master)](https://cirrus-ci.com/github/elixir-lang/elixir)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
+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.11 | Bug fixes and security patches
| 1.10 | Security patches only
| 1.10 | Bug fixes and security patches
| 1.9 | Security patches only
| 1.8 | Security patches only
| 1.7 | Security patches only
| 1.6 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.11.2
1.10.4
-4
View File
@@ -152,10 +152,6 @@ for /d %%d in ("!SCRIPT_PATH!..\lib\*.") do (
)
:run
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 !runMode! == "iex" (
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
)
+11 -13
View File
@@ -1,9 +1,9 @@
defmodule EEx.SyntaxError do
defexception [:message, :file, :line, :column]
defexception [:message, :file, :line]
@impl true
def message(exception) do
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
"#{exception.file}:#{exception.line}: #{exception.message}"
end
end
@@ -19,17 +19,17 @@ defmodule EEx do
This module provides 3 main APIs for you to use:
1. Evaluate a string (`eval_string/3`) or a file (`eval_file/3`)
1. Evaluate a string (`eval_string`) or a file (`eval_file`)
directly. This is the simplest API to use but also the
slowest, since the code is evaluated and not compiled before.
2. Define a function from a string (`function_from_string/5`)
or a file (`function_from_file/5`). This allows you to embed
2. Define a function from a string (`function_from_string`)
or a file (`function_from_file`). This allows you to embed
the template as a function inside a module which will then
be compiled. This is the preferred API if you have access
to the template at compilation time.
3. Compile a string (`compile_string/2`) or a file (`compile_file/2`)
3. Compile a string (`compile_string`) or a file (`compile_file`)
into Elixir syntax tree. This is the API used by both functions
above and is available to you if you want to provide your own
ways of handling the compiled template.
@@ -39,14 +39,12 @@ defmodule EEx do
All functions in this module accept EEx-related options.
They are:
* `:line` - the line to be used as the template start. Defaults to 1.
* `:file` - the file to be used in the template. Defaults to the given
file the template is read from or to "nofile" when compiling from a string.
* `:line` - the line to be used as the template start. Defaults to 1.
* `:indentation` - (since v1.11.0) an integer added to the column after every
new line. Defaults to 0.
* `:engine` - the EEx engine to be used for compilation.
* `:trim` - if true, trims whitespace left/right of quotation tags up until
newlines. At least one newline is retained. Defaults to false.
* `:trim` - trims whitespace left/right of quotation tags. If a quotation
tag appears on its own in a given line, line endings are also removed.
## Engine
@@ -69,7 +67,7 @@ defmodule EEx do
**must** use the equals sign (`=`). Since everything in
Elixir is an expression, there are no exceptions for this rule.
For example, while some template languages would special-case
`if` clauses, they are treated the same in EEx and
`if/2` clauses, they are treated the same in EEx and
also require `=` in order to have their result printed:
<%= if true do %>
@@ -84,7 +82,7 @@ defmodule EEx do
will be rendered as `<%= x + 3 %>`.
Note that different engines may have different rules
Notice that different engines may have different rules
for each tag. Other tags may be added in future versions.
### Macros
+25 -57
View File
@@ -13,28 +13,23 @@ defmodule EEx.Compiler do
def compile(source, opts) when is_binary(source) and is_list(opts) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
column = 1
indentation = opts[:indentation] || 0
trim = opts[:trim] || false
tokenizer_options = %{trim: trim, indentation: indentation}
case EEx.Tokenizer.tokenize(source, line, column, tokenizer_options) do
case EEx.Tokenizer.tokenize(source, line, trim: trim) do
{:ok, tokens} ->
state = %{
engine: opts[:engine] || @default_engine,
file: file,
line: line,
quoted: [],
start_line: nil,
start_column: nil,
parser_options: Code.get_compiler_option(:parser_options)
start_line: nil
}
init = state.engine.init(opts)
generate_buffer(tokens, init, [], state)
{:error, line, column, message} ->
raise EEx.SyntaxError, file: file, line: line, column: column, message: message
{:error, line, message} ->
raise EEx.SyntaxError, line: line, file: file, message: message
end
end
@@ -46,19 +41,13 @@ defmodule EEx.Compiler do
generate_buffer(rest, buffer, scope, state)
end
defp generate_buffer([{:expr, line, column, mark, chars} | rest], buffer, scope, state) do
options = [file: state.file, line: line, column: column(column, mark)] ++ state.parser_options
expr = Code.string_to_quoted!(chars, options)
defp generate_buffer([{:expr, line, mark, chars, _} | rest], buffer, scope, state) do
expr = Code.string_to_quoted!(chars, line: line, file: state.file)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
generate_buffer(rest, buffer, scope, state)
end
defp generate_buffer(
[{:start_expr, start_line, start_column, mark, chars} | rest],
buffer,
scope,
state
) do
defp generate_buffer([{:start_expr, start_line, mark, chars, _} | rest], buffer, scope, state) do
{contents, line, rest} = look_ahead_middle(rest, start_line, chars)
{contents, rest} =
@@ -66,13 +55,7 @@ defmodule EEx.Compiler do
rest,
state.engine.handle_begin(buffer),
[contents | scope],
%{
state
| quoted: [],
line: line,
start_line: start_line,
start_column: column(start_column, mark)
}
%{state | quoted: [], line: line, start_line: start_line}
)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
@@ -80,7 +63,7 @@ defmodule EEx.Compiler do
end
defp generate_buffer(
[{:middle_expr, line, _column, '', chars} | rest],
[{:middle_expr, line, '', chars, _} | rest],
buffer,
[current | scope],
state
@@ -91,7 +74,7 @@ defmodule EEx.Compiler do
end
defp generate_buffer(
[{:middle_expr, line, column, modifier, chars} | t],
[{:middle_expr, line, modifier, chars, trimmed?} | t],
buffer,
[_ | _] = scope,
state
@@ -101,35 +84,27 @@ defmodule EEx.Compiler do
"please remove \"#{modifier}\" accordingly"
:elixir_errors.erl_warn(line, state.file, message)
generate_buffer([{:middle_expr, line, column, '', chars} | t], buffer, scope, state)
generate_buffer([{:middle_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
# raise EEx.SyntaxError, message: message, file: state.file, line: line
end
defp generate_buffer([{:middle_expr, line, column, _, chars} | _], _buffer, [], state) do
defp generate_buffer([{:middle_expr, line, _, chars, _} | _], _buffer, [], state) do
raise EEx.SyntaxError,
message: "unexpected middle of expression <%#{chars}%>",
file: state.file,
line: line,
column: column
line: line
end
defp generate_buffer(
[{:end_expr, line, _column, '', chars} | rest],
buffer,
[current | _],
state
) do
defp generate_buffer([{:end_expr, line, '', chars, _} | rest], buffer, [current | _], state) do
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
column = state.start_column
options = [file: state.file, line: state.start_line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(wrapped, options)
tuples = Code.string_to_quoted!(wrapped, line: state.start_line, file: state.file)
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
defp generate_buffer(
[{:end_expr, line, column, modifier, chars} | t],
[{:end_expr, line, modifier, chars, trimmed?} | t],
buffer,
[_ | _] = scope,
state
@@ -139,29 +114,27 @@ defmodule EEx.Compiler do
"expression \"<%#{modifier}#{chars}%>\", please remove \"#{modifier}\" accordingly"
:elixir_errors.erl_warn(line, state.file, message)
generate_buffer([{:end_expr, line, column, '', chars} | t], buffer, scope, state)
generate_buffer([{:end_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
# raise EEx.SyntaxError, message: message, file: state.file, line: line, column: column
# raise EEx.SyntaxError, message: message, file: state.file, line: line
end
defp generate_buffer([{:end_expr, line, column, _, chars} | _], _buffer, [], state) do
defp generate_buffer([{:end_expr, line, _, chars, _} | _], _buffer, [], state) do
raise EEx.SyntaxError,
message: "unexpected end of expression <%#{chars}%>",
file: state.file,
line: line,
column: column
line: line
end
defp generate_buffer([{:eof, _, _}], buffer, [], state) do
defp generate_buffer([], buffer, [], state) do
state.engine.handle_body(buffer)
end
defp generate_buffer([{:eof, line, column}], _buffer, _scope, state) do
defp generate_buffer([], _buffer, _scope, state) do
raise EEx.SyntaxError,
message: "unexpected end of string, expected a closing '<% end %>'",
file: state.file,
line: line,
column: column
line: state.line
end
# Creates a placeholder and wrap it inside the expression block
@@ -179,7 +152,7 @@ defmodule EEx.Compiler do
# Look middle expressions that immediately follow a start_expr
defp look_ahead_middle(
[{:text, text}, {:middle_expr, line, _column, _, chars} | rest] = tokens,
[{:text, text}, {:middle_expr, line, _, chars, _} | rest] = tokens,
start,
contents
) do
@@ -190,7 +163,7 @@ defmodule EEx.Compiler do
end
end
defp look_ahead_middle([{:middle_expr, line, _column, _, chars} | rest], _start, contents) do
defp look_ahead_middle([{:middle_expr, line, _, chars, _} | rest], _start, contents) do
{contents ++ chars, line, rest}
end
@@ -224,9 +197,4 @@ defmodule EEx.Compiler do
defp insert_quoted(other, _quoted) do
other
end
defp column(column, mark) do
# length('<%') == 2
column + 2 + length(mark)
end
end
+151 -117
View File
@@ -3,14 +3,14 @@ defmodule EEx.Tokenizer do
@type content :: IO.chardata()
@type line :: non_neg_integer
@type column :: non_neg_integer
@type marker :: '=' | '/' | '|' | ''
@type trimmed? :: boolean
@type token ::
{:text, content}
| {:expr | :start_expr | :middle_expr | :end_expr, line, column, marker, content}
| {:eof, line, column}
| {:expr | :start_expr | :middle_expr | :end_expr, line, marker, content, trimmed?}
@spaces [?\s, ?\t]
@closing_brackets ')]}'
@doc """
Tokenizes the given charlist or binary.
@@ -18,79 +18,68 @@ defmodule EEx.Tokenizer do
It returns {:ok, list} with the following tokens:
* `{:text, content}`
* `{:expr, line, column, marker, content}`
* `{:start_expr, line, column, marker, content}`
* `{:middle_expr, line, column, marker, content}`
* `{:end_expr, line, column, marker, content}`
* `{:eof, line, column}`
* `{:expr, line, marker, content, trimmed?}`
* `{:start_expr, line, marker, content, trimmed?}`
* `{:middle_expr, line, marker, content, trimmed?}`
* `{:end_expr, line, marker, content, trimmed?}`
Or `{:error, line, column, message}` in case of errors.
Or `{:error, line, error}` in case of errors.
"""
@spec tokenize(binary | charlist, line, column, map) ::
{:ok, [token]} | {:error, line, column, String.t()}
@spec tokenize(binary | charlist, line, keyword) :: {:ok, [token]} | {:error, line, String.t()}
def tokenize(bin, line, opts \\ [])
def tokenize(bin, line, column, opts) when is_binary(bin) do
tokenize(String.to_charlist(bin), line, column, opts)
def tokenize(bin, line, opts)
when is_binary(bin) and is_integer(line) and line >= 0 and is_list(opts) do
tokenize(String.to_charlist(bin), line, opts)
end
def tokenize(list, line, column, opts)
when is_list(list) and is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
column = opts.indentation + column
{list, line, column} = (opts.trim && trim_init(list, line, column)) || {list, line, column}
tokenize(list, line, column, opts, [], [])
def tokenize(list, line, opts)
when is_list(list) and is_integer(line) and line >= 0 and is_list(opts) do
tokenize(list, line, opts, [], [])
end
defp tokenize('<%%' ++ t, line, column, opts, buffer, acc) do
tokenize(t, line, column + 3, opts, [?%, ?< | buffer], acc)
defp tokenize('<%%' ++ t, line, opts, buffer, acc) do
tokenize(t, line, opts, [?%, ?< | buffer], acc)
end
defp tokenize('<%#' ++ t, line, column, opts, buffer, acc) do
case expr(t, line, column + 3, opts, []) do
{:error, _, _, _} = error ->
defp tokenize('<%#' ++ t, line, opts, buffer, acc) do
case expr(t, line, []) do
{:error, _, _} = error ->
error
{:ok, _, new_line, new_column, rest} ->
{rest, new_line, new_column, buffer} =
trim_if_needed(rest, new_line, new_column, opts, buffer)
tokenize(rest, new_line, new_column, opts, buffer, acc)
{:ok, _, new_line, rest} ->
{_, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
tokenize(rest, new_line, opts, buffer, acc)
end
end
defp tokenize('<%' ++ t, line, column, opts, buffer, acc) do
defp tokenize('<%' ++ t, line, opts, buffer, acc) do
{marker, t} = retrieve_marker(t)
case expr(t, line, column + 2 + length(marker), opts, []) do
{:error, _, _, _} = error ->
case expr(t, line, []) do
{:error, _, _} = error ->
error
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, tokens} -> token_key(tokens, expr)
{:error, _, _, _} -> {:expr, expr}
end
{rest, new_line, new_column, buffer} =
trim_if_needed(rest, new_line, new_column, opts, buffer)
{:ok, expr, new_line, rest} ->
token = token_name(expr)
{trimmed?, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
expr = pad_if_needed(token, expr, trimmed?)
acc = tokenize_text(buffer, acc)
final = {key, line, column, marker, expr}
tokenize(rest, new_line, new_column, opts, [], [final | acc])
final = {token, line, marker, Enum.reverse(expr), trimmed?}
tokenize(rest, new_line, opts, [], [final | acc])
end
end
defp tokenize('\n' ++ t, line, _column, opts, buffer, acc) do
tokenize(t, line + 1, opts.indentation + 1, opts, [?\n | buffer], acc)
defp tokenize('\n' ++ t, line, opts, buffer, acc) do
tokenize(t, line + 1, opts, [?\n | buffer], acc)
end
defp tokenize([h | t], line, column, opts, buffer, acc) do
tokenize(t, line, column + 1, opts, [h | buffer], acc)
defp tokenize([h | t], line, opts, buffer, acc) do
tokenize(t, line, opts, [h | buffer], acc)
end
defp tokenize([], line, column, _opts, buffer, acc) do
eof = {:eof, line, column}
{:ok, Enum.reverse([eof | tokenize_text(buffer, acc)])}
defp tokenize([], _line, _opts, buffer, acc) do
{:ok, Enum.reverse(tokenize_text(buffer, acc))}
end
# Retrieve marker for <%
@@ -105,61 +94,102 @@ defmodule EEx.Tokenizer do
# Tokenize an expression until we find %>
defp expr([?%, ?> | t], line, column, _opts, buffer) do
{:ok, Enum.reverse(buffer), line, column + 2, t}
defp expr([?%, ?> | t], line, buffer) do
{:ok, buffer, line, t}
end
defp expr('\n' ++ t, line, _column, opts, buffer) do
expr(t, line + 1, opts.indentation + 1, opts, [?\n | buffer])
defp expr('\n' ++ t, line, buffer) do
expr(t, line + 1, [?\n | buffer])
end
defp expr([h | t], line, column, opts, buffer) do
expr(t, line, column + 1, opts, [h | buffer])
defp expr([h | t], line, buffer) do
expr(t, line, [h | buffer])
end
defp expr([], line, column, _opts, _buffer) do
{:error, line, column, "missing token '%>'"}
defp expr([], line, _buffer) do
{:error, line, "missing token '%>'"}
end
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(tokens, expr) do
case {tokens, Enum.reverse(tokens)} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
# Receive an expression content and check
# if it is a start, middle or an end token.
#
# Start tokens finish with "do" and "fn ->"
# Middle tokens are marked with "->" or keywords
# End tokens contain only the end word and optionally
# combinations of ")", "]" and "}".
{_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr)}
defp token_name([h | t]) when h in @spaces do
token_name(t)
end
{_, [{:block_identifier, _, _} | _]} ->
{:middle_expr, maybe_append_space(expr)}
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
{:middle_expr, expr}
{_, [{:stab_op, _, _} | reverse_tokens]} ->
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if end_index > fn_index do
{:start_expr, expr}
else
{:middle_expr, expr}
end
{tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr}
end
defp token_name('od' ++ [h | rest]) when h in @spaces or h in @closing_brackets do
case tokenize_rest(rest) do
{:ok, [{:end, _} | _]} -> :middle_expr
_ -> :start_expr
end
end
defp maybe_append_space([?\s]), do: [?\s]
defp maybe_append_space([h]), do: [h, ?\s]
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
defp token_name('>-' ++ rest) do
case tokenize_rest(rest) do
{:ok, [{:end, _} | _]} ->
:middle_expr
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
defp closing_bracket?(_), do: false
# Check if there is a "fn" token and, if so, it is not
# followed by an "end" token. If this is the case, we
# are on a start expr.
{:ok, tokens} ->
tokens = Enum.reverse(tokens)
fn_index = fn_index(tokens)
if fn_index && end_index(tokens) > fn_index do
:start_expr
else
:middle_expr
end
_error ->
:middle_expr
end
end
defp token_name('esle' ++ t), do: check_spaces(t, :middle_expr)
defp token_name('retfa' ++ t), do: check_spaces(t, :middle_expr)
defp token_name('hctac' ++ t), do: check_spaces(t, :middle_expr)
defp token_name('eucser' ++ t), do: check_spaces(t, :middle_expr)
defp token_name(rest) do
case Enum.drop_while(rest, &(&1 in @spaces or &1 in @closing_brackets)) do
'dne' ++ t -> check_spaces(t, :end_expr)
_ -> :expr
end
end
# Tokenize the remaining passing check_terminators as false,
# which relax the tokenizer to not error on unmatched pairs.
# If the tokens start with an "end" we have a middle expr.
defp tokenize_rest(rest) do
:elixir_tokenizer.tokenize(Enum.reverse(rest), 1, file: "eex", check_terminators: false)
end
defp fn_index(tokens) do
Enum.find_index(tokens, fn
{:fn_paren, _} -> true
{:fn, _} -> true
_ -> false
end)
end
defp end_index(tokens) do
Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
end
defp check_spaces(string, token) do
if Enum.all?(string, &(&1 in @spaces)) do
token
else
:expr
end
end
# Tokenize the buffered text by appending
# it to the given accumulator.
@@ -172,41 +202,45 @@ defmodule EEx.Tokenizer do
[{:text, Enum.reverse(buffer)} | acc]
end
defp trim_if_needed(rest, line, column, opts, buffer) do
if opts.trim do
buffer = trim_left(buffer, 0)
{rest, line, column} = trim_right(rest, line, column, 0)
{rest, line, column, buffer}
# If trim mode is enabled and the token is on a line with
# only itself and whitespace, trim the whitespace around it,
# including the line break following it if there is one.
defp trim_if_needed(rest, line, opts, buffer, acc) do
with true <- opts[:trim],
{true, new_buffer} <- trim_left(buffer, acc),
{true, new_rest, new_line} <- trim_right(rest, line) do
{true, new_rest, new_line, new_buffer}
else
{rest, line, column, buffer}
_ -> {false, rest, line, buffer}
end
end
defp trim_init([h | t], line, column) when h in @spaces, do: trim_init(t, line, column + 1)
defp trim_init([?\r, ?\n | t], line, _column), do: trim_init(t, line + 1, 1)
defp trim_init([?\n | t], line, _column), do: trim_init(t, line + 1, 1)
defp trim_init([?<, ?% | _] = rest, line, column), do: {rest, line, column}
defp trim_init(_, _, _), do: false
defp trim_left(buffer, count) do
case trim_whitespace(buffer) do
[?\n, ?\r | rest] -> trim_left(rest, count + 1)
[?\n | rest] -> trim_left(rest, count + 1)
_ when count > 0 -> [?\n | buffer]
_ -> buffer
defp trim_left(buffer, acc) do
case {trim_whitespace(buffer), acc} do
{[?\n | _] = trimmed_buffer, _} -> {true, trimmed_buffer}
{[], [{_, _, _, _, true} | _]} -> {true, []}
{[], []} -> {true, []}
_ -> {false, buffer}
end
end
defp trim_right(rest, line, column, count) do
defp trim_right(rest, line) do
case trim_whitespace(rest) do
[?\r, ?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
[?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
[] -> {[], line, column + length(rest)}
_ when count > 0 -> {[?\n | rest], line - 1, column}
_ -> {rest, line, column}
[?\r, ?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
[?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
[] -> {true, [], line}
_ -> {false, rest, line}
end
end
defp trim_whitespace([h | t]) when h in @spaces, do: trim_whitespace(t)
defp trim_whitespace(list), do: list
defp trim_whitespace([h | t]) when h in @spaces do
trim_whitespace(t)
end
defp trim_whitespace(list) do
list
end
defp pad_if_needed(:start_expr, [h | _] = expr, true) when h not in @spaces, do: [?\s | expr]
defp pad_if_needed(_, expr, _), do: expr
end
+71 -119
View File
@@ -4,39 +4,37 @@ defmodule EEx.TokenizerTest do
use ExUnit.Case, async: true
require EEx.Tokenizer, as: T
@opts %{indentation: 0, trim: false}
test "simple chars lists" do
assert T.tokenize('foo', 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
assert T.tokenize('foo', 1) == {:ok, [{:text, 'foo'}]}
end
test "simple strings" do
assert T.tokenize("foo", 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
assert T.tokenize("foo", 1) == {:ok, [{:text, 'foo'}]}
end
test "strings with embedded code" do
assert T.tokenize('foo <% bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '', ' bar '}, {:eof, 1, 14}]}
assert T.tokenize('foo <% bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '', ' bar ', false}]}
end
test "strings with embedded equals code" do
assert T.tokenize('foo <%= bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '=', ' bar '}, {:eof, 1, 15}]}
assert T.tokenize('foo <%= bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '=', ' bar ', false}]}
end
test "strings with embedded slash code" do
assert T.tokenize('foo <%/ bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '/', ' bar '}, {:eof, 1, 15}]}
assert T.tokenize('foo <%/ bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '/', ' bar ', false}]}
end
test "strings with embedded pipe code" do
assert T.tokenize('foo <%| bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '|', ' bar '}, {:eof, 1, 15}]}
assert T.tokenize('foo <%| bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '|', ' bar ', false}]}
end
test "strings with more than one line" do
assert T.tokenize('foo\n<%= bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo\n'}, {:expr, 2, 1, '=', ' bar '}, {:eof, 2, 11}]}
assert T.tokenize('foo\n<%= bar %>', 1) ==
{:ok, [{:text, 'foo\n'}, {:expr, 2, '=', ' bar ', false}]}
end
test "strings with more than one line and expression with more than one line" do
@@ -49,208 +47,167 @@ defmodule EEx.TokenizerTest do
exprs = [
{:text, 'foo '},
{:expr, 1, 5, '=', ' bar\n\nbaz '},
{:expr, 1, '=', ' bar\n\nbaz ', false},
{:text, '\n'},
{:expr, 4, 1, '', ' foo '},
{:text, '\n'},
{:eof, 5, 1}
{:expr, 4, '', ' foo ', false},
{:text, '\n'}
]
assert T.tokenize(string, 1, 1, @opts) == {:ok, exprs}
assert T.tokenize(string, 1) == {:ok, exprs}
end
test "quotation" do
assert T.tokenize('foo <%% true %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo <% true %>'}, {:eof, 1, 16}]}
assert T.tokenize('foo <%% true %>', 1) == {:ok, [{:text, 'foo <% true %>'}]}
end
test "quotation with do/end" do
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}, {:eof, 1, 32}]}
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1) ==
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}]}
end
test "quotation with interpolation" do
exprs = [
{:text, 'a <% b '},
{:expr, 1, 9, '=', ' c '},
{:expr, 1, '=', ' c ', false},
{:text, ' '},
{:expr, 1, 18, '=', ' d '},
{:text, ' e %> f'},
{:eof, 1, 33}
{:expr, 1, '=', ' d ', false},
{:text, ' e %> f'}
]
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1, 1, @opts) == {:ok, exprs}
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1) == {:ok, exprs}
end
test "improperly formatted quotation with interpolation" do
exprs = [
{:text, '<%% a <%= b %> c %>'},
{:eof, 1, 22}
{:text, '<%% a <%= b %> c %>'}
]
assert T.tokenize('<%%% a <%%= b %> c %>', 1, 1, @opts) == {:ok, exprs}
assert T.tokenize('<%%% a <%%= b %> c %>', 1) == {:ok, exprs}
end
test "eex comments" do
test "comments" do
exprs = [
{:text, 'foo '},
{:eof, 1, 16}
{:text, 'foo '}
]
assert T.tokenize('foo <%# true %>', 1, 1, @opts) == {:ok, exprs}
assert T.tokenize('foo <%# true %>', 1) == {:ok, exprs}
end
test "eex comments with do/end" do
test "comments with do/end" do
exprs = [
{:text, 'foo bar'},
{:eof, 1, 32}
{:text, 'foo bar'}
]
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1, 1, @opts) == {:ok, exprs}
end
test "elixir comments" do
exprs = [
{:text, 'foo '},
{:expr, 1, 5, [], ' true # this is a boolean '},
{:eof, 1, 35}
]
assert T.tokenize('foo <% true # this is a boolean %>', 1, 1, @opts) == {:ok, exprs}
end
test "elixir comments with do/end" do
exprs = [
{:start_expr, 1, 1, [], ' if true do # startif '},
{:text, 'text'},
{:end_expr, 1, 31, [], ' end # closeif '},
{:eof, 1, 50}
]
assert T.tokenize('<% if true do # startif %>text<% end # closeif %>', 1, 1, @opts) ==
{:ok, exprs}
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == {:ok, exprs}
end
test "strings with embedded do end" do
exprs = [
{:text, 'foo '},
{:start_expr, 1, 5, '', ' if true do '},
{:start_expr, 1, '', ' if true do ', false},
{:text, 'bar'},
{:end_expr, 1, 24, '', ' end '},
{:eof, 1, 33}
{:end_expr, 1, '', ' end ', false}
]
assert T.tokenize('foo <% if true do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == {:ok, exprs}
end
test "strings with embedded -> end" do
exprs = [
{:text, 'foo '},
{:start_expr, 1, 5, '', ' cond do '},
{:middle_expr, 1, 18, '', ' false -> '},
{:start_expr, 1, '', ' cond do ', false},
{:middle_expr, 1, '', ' false -> ', false},
{:text, 'bar'},
{:middle_expr, 1, 35, '', ' true -> '},
{:middle_expr, 1, '', ' true -> ', false},
{:text, 'baz'},
{:end_expr, 1, 51, '', ' end '},
{:eof, 1, 60}
{:end_expr, 1, '', ' end ', false}
]
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1, 1, @opts) ==
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) ==
{:ok, exprs}
end
test "strings with multiple callbacks" do
exprs = [
{:start_expr, 1, 1, '=', ' a fn -> '},
{:start_expr, 1, '=', ' a fn -> ', false},
{:text, 'foo'},
{:middle_expr, 1, 18, '', ' end, fn -> '},
{:middle_expr, 1, '', ' end, fn -> ', false},
{:text, 'bar'},
{:end_expr, 1, 37, '', ' end '},
{:eof, 1, 46}
{:end_expr, 1, '', ' end ', false}
]
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1, 1, @opts) ==
{:ok, exprs}
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1) == {:ok, exprs}
end
test "strings with callback followed by do block" do
exprs = [
{:start_expr, 1, 1, '=', ' a fn -> '},
{:start_expr, 1, '=', ' a fn -> ', false},
{:text, 'foo'},
{:middle_expr, 1, 18, '', ' end do '},
{:middle_expr, 1, '', ' end do ', false},
{:text, 'bar'},
{:end_expr, 1, 33, '', ' end '},
{:eof, 1, 42}
{:end_expr, 1, '', ' end ', false}
]
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1) == {:ok, exprs}
end
test "strings with embedded keywords blocks" do
exprs = [
{:text, 'foo '},
{:start_expr, 1, 5, '', ' if true do '},
{:start_expr, 1, '', ' if true do ', false},
{:text, 'bar'},
{:middle_expr, 1, 24, '', ' else '},
{:middle_expr, 1, '', ' else ', false},
{:text, 'baz'},
{:end_expr, 1, 37, '', ' end '},
{:eof, 1, 46}
{:end_expr, 1, '', ' end ', false}
]
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1, 1, @opts) ==
{:ok, exprs}
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == {:ok, exprs}
end
test "trim mode" do
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> \n\n '
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> '
exprs = [
{:start_expr, 1, 2, '=', ' if true do '},
{:text, '\n TRUE \n'},
{:middle_expr, 3, 3, '', ' else '},
{:text, '\n FALSE \n'},
{:end_expr, 5, 3, '', ' end '},
{:eof, 7, 3}
{:start_expr, 1, '=', ' if true do ', true},
{:text, ' TRUE \n'},
{:middle_expr, 3, '', ' else ', true},
{:text, ' FALSE \n'},
{:end_expr, 5, '', ' end ', true}
]
assert T.tokenize(template, 1, 1, %{@opts | trim: true}) == {:ok, exprs}
assert T.tokenize(template, 1, trim: true) == {:ok, exprs}
end
test "trim mode with comment" do
exprs = [
{:text, '\n123'},
{:eof, 2, 4}
{:text, '123'}
]
assert T.tokenize(' <%# comment %> \n123', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
assert T.tokenize(' <%# comment %> \n123', 1, trim: true) == {:ok, exprs}
end
test "trim mode with CRLF" do
exprs = [
{:text, '0\n'},
{:expr, 2, 3, '=', ' 12 '},
{:text, '\n34'},
{:eof, 3, 3}
{:text, '0\r\n'},
{:expr, 2, '=', ' 12 ', true},
{:text, '34'}
]
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, trim: true) == {:ok, exprs}
end
test "trim mode set to false" do
exprs = [
{:text, ' '},
{:expr, 1, 2, '=', ' 12 '},
{:text, ' \n'},
{:eof, 2, 1}
{:expr, 1, '=', ' 12 ', false},
{:text, ' \n'}
]
assert T.tokenize(' <%= 12 %> \n', 1, 1, %{@opts | trim: false}) == {:ok, exprs}
assert T.tokenize(' <%= 12 %> \n', 1, trim: false) == {:ok, exprs}
end
test "trim mode no false positives" do
assert_not_trimmed = fn x ->
assert T.tokenize(x, 1, 1, %{@opts | trim: false}) == T.tokenize(x, 1, 1, @opts)
end
assert_not_trimmed = fn x -> assert T.tokenize(x, 1, trim: true) == T.tokenize(x, 1) end
assert_not_trimmed.('foo <%= "bar" %> ')
assert_not_trimmed.('\n <%= "foo" %>bar')
@@ -258,13 +215,8 @@ defmodule EEx.TokenizerTest do
assert_not_trimmed.(' <%= 01 %><%= 23 %>\n')
end
test "returns error when there is start mark and no end mark" do
assert T.tokenize('foo <% :bar', 1, 1, @opts) == {:error, 1, 12, "missing token '%>'"}
assert T.tokenize('<%# true ', 1, 1, @opts) == {:error, 1, 10, "missing token '%>'"}
end
test "marks invalid expressions as regular expressions" do
assert T.tokenize('<% 1 $ 2 %>', 1, 1, @opts) ==
{:ok, [{:expr, 1, 1, [], ' 1 $ 2 '}, {:eof, 1, 12}]}
test "raise syntax error when there is start mark and no end mark" do
assert T.tokenize('foo <% :bar', 1) == {:error, 1, "missing token '%>'"}
assert T.tokenize('<%# true ', 1) == {:error, 1, "missing token '%>'"}
end
end
+13 -77
View File
@@ -79,24 +79,8 @@ defmodule EExTest do
end
test "trim mode" do
string = "<%= 123 %> \n \n <%= 789 %>"
expected = "123\n789"
assert_eval(expected, string, [], trim: true)
string = "<%= 123 %> \n456\n <%= 789 %>"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
string = "<%= 123 %> \n <%= 456 %> \n <%= 789 %>"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
string = "\n <%= 123 %> \n <%= 456 %> \n <%= 789 %> \n"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
string = "\r\n <%= 123 %> \r\n <%= 456 %> \r\n <%= 789 %> \r\n"
expected = "123\n456\n789"
expected = "123456\n789"
assert_eval(expected, string, [], trim: true)
end
@@ -110,7 +94,7 @@ defmodule EExTest do
<% end %>
"""
expected = "\n that\n"
expected = " that\n"
assert_eval(expected, string, [], trim: true)
end
@@ -122,22 +106,11 @@ defmodule EExTest do
<%= "Fourth line" %>
"""
expected = "First line\nSecond line\nThird line\nFourth line"
expected = "First lineSecond lineThird lineFourth line"
assert_eval(expected, string, [], trim: true)
end
test "trim mode with no spaces" do
string = """
<%=if true do%>
this
<%else%>
that
<%end%>
"""
expected = "\n this\n"
assert_eval(expected, string, [], trim: true)
string = """
<%=cond do%>
<%false ->%>
@@ -147,7 +120,7 @@ defmodule EExTest do
<%end%>
"""
expected = "\n that\n"
expected = " that\n"
assert_eval(expected, string, [], trim: true)
end
@@ -247,27 +220,25 @@ defmodule EExTest do
describe "raises syntax errors" do
test "when the token is invalid" do
assert_raise EEx.SyntaxError, "nofile:1:12: missing token '%>'", fn ->
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
EEx.compile_string("foo <%= bar")
end
end
test "when middle expression is found without a start expression" do
assert_raise EEx.SyntaxError,
"nofile:1:18: unexpected middle of expression <% else %>",
fn ->
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
end
assert_raise EEx.SyntaxError, "nofile:1: unexpected middle of expression <% else %>", fn ->
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
end
end
test "when end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1:5: unexpected end of expression <% end %>", fn ->
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
EEx.compile_string("foo <% end %>")
end
end
test "when start expression is found without an end expression" do
msg = "nofile:2:17: unexpected end of string, expected a closing '<% end %>'"
msg = "nofile:2: unexpected end of string, expected a closing '<% end %>'"
assert_raise EEx.SyntaxError, msg, fn ->
EEx.compile_string("foo\n<% if true do %>")
@@ -275,7 +246,7 @@ defmodule EExTest do
end
test "when nested end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1:30: unexpected end of expression <% end %>", fn ->
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
EEx.compile_string("foo <% if true do %><% end %><% end %>")
end
end
@@ -314,13 +285,13 @@ defmodule EExTest do
describe "error messages" do
test "honor line numbers" do
assert_raise EEx.SyntaxError, "nofile:99:12: missing token '%>'", fn ->
assert_raise EEx.SyntaxError, "nofile:99: missing token '%>'", fn ->
EEx.compile_string("foo <%= bar", line: 99)
end
end
test "honor file names" do
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
assert_raise EEx.SyntaxError, "my_file.eex:1: missing token '%>'", fn ->
EEx.compile_string("foo <%= bar", file: "my_file.eex")
end
end
@@ -531,41 +502,6 @@ defmodule EExTest do
assert_eval("\n\n Good\n \n", string)
end
test "line and column meta" do
parser_options = Code.get_compiler_option(:parser_options)
Code.put_compiler_option(:parser_options, columns: true)
try do
indentation = 12
ast =
EEx.compile_string(
"""
<%= f() %> <% f() %>
<%= f fn -> %>
<%= f() %>
<% end %>
""",
indentation: indentation
)
{_, calls} =
Macro.prewalk(ast, [], fn
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
other, acc -> {other, acc}
end)
assert Enum.reverse(calls) == [
[line: 1, column: indentation + 5],
[line: 1, column: indentation + 15],
[line: 2, column: indentation + 7],
[line: 3, column: indentation + 9]
]
after
Code.put_compiler_option(:parser_options, parser_options)
end
end
end
describe "buffers" do
+1
View File
@@ -11,6 +11,7 @@
warn_exported_vars,
%% warn_missing_spec,
%% warn_untyped_record,
warnings_as_errors,
debug_info,
{outdir, "ebin/"}
]}.
-156
View File
@@ -1,156 +0,0 @@
defmodule Diff do
@moduledoc """
Utilities for comparing build artifacts.
"""
@known_chunks ~w(
abstract_code
debug_info
attributes
compile_info
exports
labeled_exports
imports
indexed_imports
locals
labeled_locals
atoms
)a
@doc """
Compares the build artifacts of two build directories.
"""
@spec compare_dirs(Path.t(), Path.t()) ::
{
only1_paths :: list(Path.t()),
only2_paths :: list(Path.t()),
diff :: list({Path.t(), diff :: String.t()})
}
def compare_dirs(dir1, dir2) do
dir1 = Path.expand(dir1)
dir2 = Path.expand(dir2)
assert_dir!(dir1)
assert_dir!(dir2)
dir1_paths = relative_paths(dir1)
dir2_paths = relative_paths(dir2)
only1_paths = dir1_paths -- dir2_paths
only2_paths = dir2_paths -- dir1_paths
common_paths = dir1_paths -- only1_paths
common_files = Enum.reject(common_paths, &File.dir?/1)
diff =
Enum.flat_map(common_files, fn path ->
file1 = Path.join(dir1, path)
file2 = Path.join(dir2, path)
case compare_files(file1, file2) do
:eq -> []
{:diff, diff} -> [{path, diff}]
end
end)
{only1_paths, only2_paths, diff}
end
@doc """
Compares the contents of two files.
If the files are BEAM files, it performs a more human-friendly
"BEAM-diff".
"""
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
def compare_files(file1, file2) do
content1 = File.read!(file1)
content2 = File.read!(file2)
if content1 == content2 do
:eq
else
diff =
if String.ends_with?(file1, ".beam") do
beam_diff(file1, content1, file2, content2)
else
file_diff(file1, file2)
end
{:diff, diff}
end
end
defp beam_diff(file1, content1, file2, content2) do
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks),
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks),
true <- chunks1 != chunks2 do
for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do
tmp_file1 =
chunk1
|> inspect(pretty: true, limit: :infinity)
|> write_tmp()
tmp_file2 =
chunk2
|> inspect(pretty: true, limit: :infinity)
|> write_tmp()
file_diff(tmp_file1, tmp_file2)
end
else
_ ->
file_diff(file1, file2)
end
end
defp file_diff(file1, file2) do
{diff, _} = System.cmd("diff", [file1, file2])
diff
end
defp relative_paths(dir) do
dir
|> Path.join("**")
|> Path.wildcard()
|> Enum.map(&Path.relative_to(&1, dir))
end
defp assert_dir!(dir) do
unless File.dir?(dir) do
raise ArgumentError, "#{inspect(dir)} is not a directory"
end
end
defp write_tmp(content) do
filename = generate_tmp_filename()
File.mkdir_p!("tmp")
File.write!(Path.join("tmp", filename), content)
Path.join("tmp", filename)
end
defp generate_tmp_filename do
sec = :os.system_time(:second)
rand = :rand.uniform(999_999_999)
scheduler_id = :erlang.system_info(:scheduler_id)
"tmp-#{sec}-#{rand}-#{scheduler_id}"
end
end
case System.argv() do
[dir1, dir2] ->
case Diff.compare_dirs(dir1, dir2) do
{[], [], []} ->
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
{only1, only2, diff} ->
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
System.halt(1)
end
_ ->
IO.puts("Please, provide two directories as arguments")
System.halt(1)
end
+11 -14
View File
@@ -1,10 +1,10 @@
# Returns config for Elixir docs
[
extras: Path.wildcard("lib/elixir/pages/*.md") ++ ["CHANGELOG.md"],
extras: Path.wildcard("lib/elixir/pages/*.md"),
groups_for_functions: [
Guards: & &1[:guard] == true
],
skip_undefined_reference_warnings_on: ["lib/elixir/pages/compatibility-and-deprecations.md"],
skip_undefined_reference_warnings_on: ["compatibility-and-deprecations"],
groups_for_modules: [
# [Kernel, Kernel.SpecialForms],
@@ -89,18 +89,15 @@
Kernel.ParallelCompiler,
Macro,
Macro.Env
],
Deprecated: [
Behaviour,
Dict,
GenEvent,
HashDict,
HashSet,
Set,
Supervisor.Spec
]
## Automatically detected groups
# Deprecated: [
# Behaviour,
# Dict,
# GenEvent,
# HashDict,
# HashSet,
# Set,
# Supervisor.Spec
# ]
]
]
+11 -43
View File
@@ -6,8 +6,8 @@ defmodule Access do
keys of any type in a data structure via the `data[key]` syntax.
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
of the box. Keywords supports only atoms keys, keys for maps can
be of any type. Both returns `nil` if the key does not exist:
of the box. The key can be of any type and it returns `nil` if
the key does not exist:
iex> keywords = [a: 1, b: 2]
iex> keywords[:a]
@@ -683,68 +683,36 @@ defmodule Access do
end
defp at(:get_and_update, data, index, next) when is_list(data) do
get_and_update_at(data, index, next, [], fn -> nil end)
get_and_update_at(data, index, next, [])
end
defp at(_op, data, _index, _next) do
raise "Access.at/1 expected a list, got: #{inspect(data)}"
end
defp get_and_update_at([head | rest], 0, next, updates, _default_fun) do
defp get_and_update_at([head | rest], 0, next, updates) do
case next.(head) do
{get, update} -> {get, :lists.reverse([update | updates], rest)}
:pop -> {head, :lists.reverse(updates, rest)}
end
end
defp get_and_update_at([_ | _] = list, index, next, updates, default_fun) when index < 0 do
defp get_and_update_at(list, index, next, updates) when index < 0 do
list_length = length(list)
if list_length + index >= 0 do
get_and_update_at(list, list_length + index, next, updates, default_fun)
get_and_update_at(list, list_length + index, next, updates)
else
{default_fun.(), list}
{nil, list}
end
end
defp get_and_update_at([head | rest], index, next, updates, default_fun) when index > 0 do
get_and_update_at(rest, index - 1, next, [head | updates], default_fun)
defp get_and_update_at([head | rest], index, next, updates) when index > 0 do
get_and_update_at(rest, index - 1, next, [head | updates])
end
defp get_and_update_at([], _index, _next, updates, default_fun) do
{default_fun.(), :lists.reverse(updates)}
end
@doc ~S"""
Same as `at/1` except that it raises `Enum.OutOfBoundsError`
if the given index is out of bounds.
## Examples
iex> get_in([:a, :b, :c], [Access.at!(2)])
:c
iex> get_in([:a, :b, :c], [Access.at!(3)])
** (Enum.OutOfBoundsError) out of bounds error
"""
@spec at!(integer) :: access_fun(data :: list, get_value :: term)
def at!(index) when is_integer(index) do
fn op, data, next -> at!(op, data, index, next) end
end
defp at!(:get, data, index, next) when is_list(data) do
case Enum.fetch(data, index) do
{:ok, value} -> next.(value)
:error -> raise Enum.OutOfBoundsError
end
end
defp at!(:get_and_update, data, index, next) when is_list(data) do
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
end
defp at!(_op, data, _index, _next) do
raise "Access.at!/1 expected a list, got: #{inspect(data)}"
defp get_and_update_at([], _index, _next, updates) do
{nil, :lists.reverse(updates)}
end
@doc ~S"""
+11 -22
View File
@@ -7,7 +7,7 @@ defmodule Application do
programming languages, but with some additional characteristics.
An application is a component implementing some specific functionality, with a
standardized directory structure, configuration, and life cycle. Applications
standardized directory structure, configuration, and lifecycle. Applications
are *loaded*, *started*, and *stopped*. Each application also has its own
environment, which provides a unified API for configuring each application.
@@ -84,8 +84,8 @@ defmodule Application do
This approach has one big limitation: if you change the value of the
application environment after the code is compiled, the value used at
runtime is not going to change! For example, if your `config/runtime.exs`
has:
runtime is not going to change! For example, if you are using `mix release`
and your `config/releases.exs` has:
config :my_app, :db_host, "db.production"
@@ -171,7 +171,7 @@ defmodule Application do
the documentation of `Mix.Tasks.Compile.App`, available as well by running
`mix help compile.app`.
## The application life cycle
## The application lifecycle
### Loading applications
@@ -210,7 +210,7 @@ defmodule Application do
dependencies for you.
If the application does not have a callback module configured, starting is
done at this point. Otherwise, its `c:start/2` callback is invoked. The PID of
done at this point. Otherwise, its `c:start/2` callback if invoked. The PID of
the top-level supervisor returned by this function is stored by the runtime
for later use, and the returned application state is saved too, if any.
@@ -499,7 +499,7 @@ defmodule Application do
Giving a path is useful to let Elixir know that only certain paths
in a large configuration are compile time dependent.
"""
# TODO: Warn on v1.14 if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
# TODO: Warn if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
@doc since: "1.10.0"
@spec compile_env(app, key | list, value) :: value
defmacro compile_env(app, key_or_path, default \\ nil) when is_atom(app) do
@@ -524,7 +524,7 @@ defmodule Application do
Reads the application environment at compilation time or raises.
This is the same as `compile_env/3` but it raises an
`ArgumentError` if the configuration is not available.
ArgumentError if the configuration is not available.
"""
@doc since: "1.10.0"
@spec compile_env!(app, key | list) :: value
@@ -584,7 +584,7 @@ defmodule Application do
**Important:** 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).
read our [library guidelines](library-guidelines.html).
## Examples
@@ -618,7 +618,6 @@ defmodule Application do
"""
@spec get_env(app, key, value) :: value
def get_env(app, key, default \\ nil) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
:application.get_env(app, key, default)
end
@@ -629,8 +628,6 @@ defmodule Application do
"""
@spec fetch_env(app, key) :: {:ok, value} | :error
def fetch_env(app, key) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
case :application.get_env(app, key) do
{:ok, value} -> {:ok, value}
:undefined -> :error
@@ -667,7 +664,9 @@ defmodule Application do
"because configuration at #{inspect(key)} was not set"
:undefined ->
"because the application was not loaded nor configured"
"because the application was not loaded/started. If your application " <>
"depends on #{inspect(app)} at runtime, make sure to load/start it or " <>
"list it under :extra_applications in your mix.exs file"
end
end
@@ -690,7 +689,6 @@ defmodule Application do
"""
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
def put_env(app, key, value, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
:application.set_env(app, key, value, opts)
end
@@ -736,18 +734,9 @@ defmodule Application do
"""
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
def delete_env(app, key, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
:application.unset_env(app, key, opts)
end
defp maybe_warn_on_app_env_key(_app, key) when is_atom(key),
do: :ok
defp maybe_warn_on_app_env_key(app, key) do
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
end
@doc """
Ensures the given `app` is started.
+2 -2
View File
@@ -111,9 +111,9 @@ defmodule Behaviour do
end
end
defp __behaviour__doc_value(%{"en" => doc}), do: doc
defp __behaviour__doc_value(:none), do: nil
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(_), do: nil
defp __behaviour__doc_value(%{"en" => doc}), do: doc
import unquote(__MODULE__)
end
+7 -19
View File
@@ -61,7 +61,7 @@ defmodule Bitwise do
end
@doc """
Calculates the bitwise NOT of the argument.
Calculates the bitwise NOT of its argument.
Allowed in guard tests. Inlined by the compiler.
@@ -81,9 +81,7 @@ defmodule Bitwise do
end
@doc """
Bitwise NOT unary operator.
Calculates the bitwise NOT of the argument.
Prefix (unary) operator; calculates the bitwise NOT of its argument.
Allowed in guard tests. Inlined by the compiler.
@@ -120,9 +118,7 @@ defmodule Bitwise do
end
@doc """
Bitwise AND operator.
Calculates the bitwise AND of its arguments.
Infix operator; calculates the bitwise AND of its arguments.
Allowed in guard tests. Inlined by the compiler.
@@ -156,9 +152,7 @@ defmodule Bitwise do
end
@doc """
Bitwise OR operator.
Calculates the bitwise OR of its arguments.
Infix operator; calculates the bitwise OR of its arguments.
Allowed in guard tests. Inlined by the compiler.
@@ -192,9 +186,7 @@ defmodule Bitwise do
end
@doc """
Bitwise XOR operator.
Calculates the bitwise XOR of its arguments.
Infix operator; calculates the bitwise XOR of its arguments.
Allowed in guard tests. Inlined by the compiler.
@@ -237,9 +229,7 @@ defmodule Bitwise do
end
@doc """
Arithmetic left bitshift operator.
Calculates the result of an arithmetic left bitshift.
Infix operator; calculates the result of an arithmetic left bitshift.
Allowed in guard tests. Inlined by the compiler.
@@ -291,9 +281,7 @@ defmodule Bitwise do
end
@doc """
Arithmetic right bitshift operator.
Calculates the result of an arithmetic right bitshift.
Infix operator; calculates the result of an arithmetic right bitshift.
Allowed in guard tests. Inlined by the compiler.
+3 -520
View File
@@ -68,18 +68,10 @@ defmodule Calendar do
@typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)"
@type zone_abbr :: String.t()
@typedoc """
The time zone UTC offset in seconds for standard time.
See also `t:std_offset/0`.
"""
@typedoc "The time zone UTC offset in seconds"
@type utc_offset :: integer
@typedoc """
The time zone standard offset in seconds (typically not zero in summer times).
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
"""
@typedoc "The time zone standard offset in seconds (not zero in summer times)"
@type std_offset :: integer
@typedoc "Any map/struct that contains the date fields"
@@ -167,14 +159,8 @@ defmodule Calendar do
@doc """
Calculates the day of the week from the given `year`, `month`, and `day`.
The `starting_on` represents the starting day of the week. All
calendars must support at least the `:default` value. They may
also support other values representing their days of the week.
"""
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
{day_of_week(), first_day_of_week :: non_neg_integer(),
last_day_of_week :: non_neg_integer()}
@callback day_of_week(year, month, day) :: day_of_week()
@doc """
Calculates the day of the year from the given `year`, `month`, and `day`.
@@ -377,507 +363,4 @@ defmodule Calendar do
def get_time_zone_database() do
Application.get_env(:elixir, :time_zone_database, Calendar.UTCOnlyTimeZoneDatabase)
end
@doc """
Formats received datetime into a string.
The datetime can be any of the Calendar types (`Time`, `Date`,
`NaiveDateTime`, and `DateTime`) or any map, as long as they
contain all of the relevant fields necessary for formatting.
For example, if you use `%Y` to format the year, the datetime
must have the `:year` field. Therefore, if you pass a `Time`,
or a map without the `:year` field to a format that expects `%Y`,
an error will be raised.
## Options
* `:preferred_datetime` - a string for the preferred format to show datetimes,
it can't contain the `%c` format and defaults to `"%Y-%m-%d %H:%M:%S"`
if the option is not received
* `:preferred_date` - a string for the preferred format to show dates,
it can't contain the `%x` format and defaults to `"%Y-%m-%d"`
if the option is not received
* `:preferred_time` - a string for the preferred format to show times,
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
if the option is not received
* `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
the name of the period of the day, if the option is not received it defaults
to a function that returns `"am"` and `"pm"`, respectively
* `:month_names` - a function that receives a number and returns the name of
the corresponding month, if the option is not received it defaults to a
function that returns the month names in English
* `:abbreviated_month_names` - a function that receives a number and returns the
abbreviated name of the corresponding month, if the option is not received it
defaults to a function that returns the abbreviated month names in English
* `:day_of_week_names` - a function that receives a number and returns the name of
the corresponding day of week, if the option is not received it defaults to a
function that returns the day of week names in English
* `:abbreviated_day_of_week_names` - a function that receives a number and returns
the abbreviated name of the corresponding day of week, if the option is not received
it defaults to a function that returns the abbreviated day of week names in English
## Formatting syntax
The formatting syntax for strftime is a sequence of characters in the following format:
%<padding><width><format>
where:
* `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section
* `<format>`: the format itself (see below)
### Accepted padding options
* `-`: no padding, removes all padding from the format
* `_`: pad with spaces
* `0`: pad with zeroes
### Accepted formats
The accepted formats are:
Format | Description | Examples (in ISO)
:----- | :-----------------------------------------------------------------------| :------------------------
a | Abbreviated name of day | Mon
A | Full name of day | Monday
b | Abbreviated month name | Jan
B | Full month name | January
c | Preferred date+time representation | 2018-10-17 12:34:56
d | Day of the month | 01, 12
f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
H | Hour using a 24-hour clock | 00, 23
I | Hour using a 12-hour clock | 01, 12
j | Day of the year | 001, 366
m | Month | 01, 12
M | Minute | 00, 59
p | "AM" or "PM" (noon is "PM", midnight as "AM") | AM, PM
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
q | Quarter | 1, 2, 3, 4
S | Second | 00, 59, 60
u | Day of the week | 1 (Monday), 7 (Sunday)
x | Preferred date (without time) representation | 2018-10-17
X | Preferred time (without date) representation | 12:34:56
y | Year as 2-digits | 01, 01, 86, 18
Y | Year | -0001, 0001, 1986
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
Z | Time zone abbreviation (empty string if naive) | CET, BRST
% | Literal "%" character | %
Any other character will be interpreted as an invalid format and raise an error
## Examples
Without options:
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%y-%m-%d %I:%M:%S %p")
"19-08-26 01:52:06 PM"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%a, %B %d %Y")
"Mon, August 26 2019"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c")
"2019-08-26 13:52:06"
With options:
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c", preferred_datetime: "%H:%M:%S %d-%m-%y")
"13:52:06 26-08-19"
iex> Calendar.strftime(
...> ~U[2019-08-26 13:52:06.0Z],
...> "%A",
...> day_of_week_names: fn day_of_week ->
...> {"segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
...> "sexta-feira", "sábado", "domingo"}
...> |> elem(day_of_week - 1)
...> end
...>)
"segunda-feira"
iex> Calendar.strftime(
...> ~U[2019-08-26 13:52:06.0Z],
...> "%B",
...> month_names: fn month ->
...> {"январь", "февраль", "март", "апрель", "май", "июнь",
...> "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"}
...> |> elem(month - 1)
...> end
...>)
"август"
"""
@doc since: "1.11.0"
@spec strftime(map(), String.t(), keyword()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse(
string_format,
date_or_time_or_datetime,
options(user_options),
[]
)
|> IO.iodata_to_binary()
end
defp parse("", _datetime, _format_options, acc),
do: Enum.reverse(acc)
defp parse("%" <> rest, datetime, format_options, acc),
do: parse_modifiers(rest, nil, nil, {datetime, format_options, acc})
defp parse(<<char, rest::binary>>, datetime, format_options, acc),
do: parse(rest, datetime, format_options, [char | acc])
defp parse_modifiers("-" <> rest, width, nil, parser_data) do
parse_modifiers(rest, width, "", parser_data)
end
defp parse_modifiers("0" <> rest, width, nil, parser_data) do
parse_modifiers(rest, width, ?0, parser_data)
end
defp parse_modifiers("_" <> rest, width, nil, parser_data) do
parse_modifiers(rest, width, ?\s, parser_data)
end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
new_width = (width || 0) * 10 + (digit - ?0)
parse_modifiers(rest, new_width, pad, parser_data)
end
# set default padding if none was specified
defp parse_modifiers(<<format, _::binary>> = rest, width, nil, parser_data) do
parse_modifiers(rest, width, default_pad(format), parser_data)
end
# set default width if none was specified
defp parse_modifiers(<<format, _::binary>> = rest, nil, pad, parser_data) do
parse_modifiers(rest, default_width(format), pad, parser_data)
end
defp parse_modifiers(rest, width, pad, {datetime, format_options, acc}) do
format_modifiers(rest, width, pad, datetime, format_options, acc)
end
defp am_pm(hour, format_options) when hour > 11 do
format_options.am_pm_names.(:pm)
end
defp am_pm(hour, format_options) when hour <= 11 do
format_options.am_pm_names.(:am)
end
defp default_pad(format) when format in 'aAbBpPZ', do: ?\s
defp default_pad(_format), do: ?0
defp default_width(format) when format in 'dHImMSy', do: 2
defp default_width(?j), do: 3
defp default_width(format) when format in 'Yz', do: 4
defp default_width(_format), do: 0
# Literally just %
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
end
# Abbreviated name of day
defp format_modifiers("a" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime
|> Date.day_of_week()
|> format_options.abbreviated_day_of_week_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Full name of day
defp format_modifiers("A" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime
|> Date.day_of_week()
|> format_options.day_of_week_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Abbreviated month name
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.month
|> format_options.abbreviated_month_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Full month name
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Preferred date+time representation
defp format_modifiers(
"c" <> _rest,
_width,
_pad,
_datetime,
%{preferred_datetime_invoked: true},
_acc
) do
raise ArgumentError,
"tried to format preferred_datetime within another preferred_datetime format"
end
defp format_modifiers("c" <> rest, width, pad, datetime, format_options, acc) do
result =
format_options.preferred_datetime
|> parse(datetime, %{format_options | preferred_datetime_invoked: true}, [])
|> pad_preferred(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the month
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Microseconds
defp format_modifiers("f" <> rest, _width, _pad, datetime, format_options, acc) do
{microsecond, precision} = datetime.microsecond
result =
microsecond
|> Integer.to_string()
|> String.pad_leading(6, "0")
|> binary_part(0, max(precision, 1))
parse(rest, datetime, format_options, [result | acc])
end
# Hour using a 24-hour clock
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the year
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Month
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Minute
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# "AM" or "PM" (noon is "PM", midnight as "AM")
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# "am" or "pm" (noon is "pm", midnight as "am")
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.hour
|> am_pm(format_options)
|> String.downcase()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Quarter
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Second
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the week
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Preferred date (without time) representation
defp format_modifiers(
"x" <> _rest,
_width,
_pad,
_datetime,
%{preferred_date_invoked: true},
_acc
) do
raise ArgumentError,
"tried to format preferred_date within another preferred_date format"
end
defp format_modifiers("x" <> rest, width, pad, datetime, format_options, acc) do
result =
format_options.preferred_date
|> parse(datetime, %{format_options | preferred_date_invoked: true}, [])
|> pad_preferred(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Preferred time (without date) representation
defp format_modifiers(
"X" <> _rest,
_width,
_pad,
_datetime,
%{preferred_time_invoked: true},
_acc
) do
raise ArgumentError,
"tried to format preferred_time within another preferred_time format"
end
defp format_modifiers("X" <> rest, width, pad, datetime, format_options, acc) do
result =
format_options.preferred_time
|> parse(datetime, %{format_options | preferred_time_invoked: true}, [])
|> pad_preferred(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Year as 2-digits
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# +hhmm/-hhmm time zone offset from UTC (empty string if naive)
defp format_modifiers(
"z" <> rest,
width,
pad,
datetime = %{utc_offset: utc_offset, std_offset: std_offset},
format_options,
acc
) do
absolute_offset = abs(utc_offset + std_offset)
offset_number =
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
parse(rest, datetime, format_options, [result | acc])
end
defp format_modifiers("z" <> rest, _width, _pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, ["" | acc])
end
# Time zone abbreviation (empty string if naive)
defp format_modifiers("Z" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Map.get(:zone_abbr, "") |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
defp format_modifiers(rest, _width, _pad, _datetime, _format_options, _acc) do
{next, _rest} = String.next_grapheme(rest) || {"", ""}
raise ArgumentError, "invalid strftime format: %#{next}"
end
defp pad_preferred(result, width, pad) when length(result) < width do
pad_preferred([pad | result], width, pad)
end
defp pad_preferred(result, _width, _pad), do: result
defp pad_leading(string, count, padding) do
to_pad = count - byte_size(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
defp do_pad_leading(0, _, acc), do: acc
defp do_pad_leading(count, padding, acc),
do: do_pad_leading(count - 1, padding, [padding | acc])
defp options(user_options) do
default_options = %{
preferred_date: "%Y-%m-%d",
preferred_time: "%H:%M:%S",
preferred_datetime: "%Y-%m-%d %H:%M:%S",
am_pm_names: fn
:am -> "am"
:pm -> "pm"
end,
month_names: fn month ->
{"January", "February", "March", "April", "May", "June", "July", "August", "September",
"October", "November", "December"}
|> elem(month - 1)
end,
day_of_week_names: fn day_of_week ->
{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|> elem(day_of_week - 1)
end,
abbreviated_month_names: fn month ->
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|> elem(month - 1)
end,
abbreviated_day_of_week_names: fn day_of_week ->
{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} |> elem(day_of_week - 1)
end,
preferred_datetime_invoked: false,
preferred_date_invoked: false,
preferred_time_invoked: false
}
Enum.reduce(user_options, default_options, fn {key, value}, acc ->
if Map.has_key?(acc, key) do
%{acc | key => value}
else
raise ArgumentError, "unknown option #{inspect(key)} given to Calendar.strftime/3"
end
end)
end
end
+9 -237
View File
@@ -88,20 +88,20 @@ defmodule Date do
"""
@doc since: "1.5.0"
@spec range(Calendar.date(), Calendar.date()) :: Date.Range.t()
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
@spec range(Date.t(), Date.t()) :: Date.Range.t()
def range(%Date{calendar: calendar} = first, %Date{calendar: calendar} = last) do
{first_days, _} = to_iso_days(first)
{last_days, _} = to_iso_days(last)
%Date.Range{
first: %Date{calendar: calendar, year: first.year, month: first.month, day: first.day},
last: %Date{calendar: calendar, year: last.year, month: last.month, day: last.day},
first: first,
last: last,
first_in_iso_days: first_days,
last_in_iso_days: last_days
}
end
def range(%{calendar: _, year: _, month: _, day: _}, %{calendar: _, year: _, month: _, day: _}) do
def range(%Date{}, %Date{}) do
raise ArgumentError, "both dates must have matching calendars"
end
@@ -224,33 +224,6 @@ defmodule Date do
end
end
@doc """
Builds a new ISO date.
Expects all values to be integers. Returns `date` if each
entry fits its appropriate range, raises if the date is invalid.
## Examples
iex> Date.new!(2000, 1, 1)
~D[2000-01-01]
iex> Date.new!(2000, 13, 1)
** (ArgumentError) cannot build date, reason: :invalid_date
iex> Date.new!(2000, 2, 29)
~D[2000-02-29]
"""
@doc since: "1.11.0"
@spec new!(Calendar.year(), Calendar.month(), Calendar.day(), Calendar.calendar()) :: t
def new!(year, month, day, calendar \\ Calendar.ISO) do
case new(year, month, day, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot build date, reason: #{inspect(reason)}"
end
end
@doc """
Converts the given date to a string according to its calendar.
@@ -425,45 +398,6 @@ defmodule Date do
end
end
@doc """
Converts a number of gregorian days to a `Date` struct.
## Examples
iex> Date.from_gregorian_days(1)
~D[0000-01-02]
iex> Date.from_gregorian_days(730_485)
~D[2000-01-01]
iex> Date.from_gregorian_days(-1)
~D[-0001-12-31]
"""
@doc since: "1.11.0"
@spec from_gregorian_days(integer(), Calendar.calendar()) :: t
def from_gregorian_days(days, calendar \\ Calendar.ISO) when is_integer(days) do
from_iso_days({days, 0}, calendar)
end
@doc """
Converts a `date` struct to a number of gregorian days.
## Examples
iex> Date.to_gregorian_days(~D[0000-01-02])
1
iex> Date.to_gregorian_days(~D[2000-01-01])
730_485
iex> Date.to_gregorian_days(~N[2000-01-01 00:00:00])
730_485
"""
@doc since: "1.11.0"
@spec to_gregorian_days(Calendar.date()) :: integer()
def to_gregorian_days(date) do
{days, _} = to_iso_days(date)
days
end
@doc """
Compares two date structs.
@@ -684,11 +618,6 @@ defmodule Date do
calendar (the default), it is an integer from 1 to 7, where
1 is Monday and 7 is Sunday.
An optional `starting_on` value may be supplied, which
configures the weekday the week starts on. The default value
for it is `:default`, which translates to `:monday` for the
built-in ISO calendar. Any other weekday may be given to.
## Examples
iex> Date.day_of_week(~D[2016-10-31])
@@ -700,131 +629,13 @@ defmodule Date do
iex> Date.day_of_week(~D[-0015-10-30])
3
iex> Date.day_of_week(~D[2016-10-31], :sunday)
2
iex> Date.day_of_week(~D[2016-11-01], :sunday)
3
iex> Date.day_of_week(~N[2016-11-01 01:23:45], :sunday)
3
iex> Date.day_of_week(~D[-0015-10-30], :sunday)
4
"""
@doc since: "1.4.0"
@spec day_of_week(Calendar.date(), starting_on :: :default | atom) :: Calendar.day_of_week()
def day_of_week(date, starting_on \\ :default)
@spec day_of_week(Calendar.date()) :: Calendar.day()
def day_of_week(date)
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}, starting_on) do
{day_of_week, _first, _last} = calendar.day_of_week(year, month, day, starting_on)
day_of_week
end
@doc """
Calculates a date that is the first day of the week for the given `date`.
If the day is already the first day of the week, it returns the
day itself. For the built-in ISO calendar, the week starts on Monday.
A weekday rather than `:default` can be given as `starting_on`.
## Examples
iex> Date.beginning_of_week(~D[2020-07-11])
~D[2020-07-06]
iex> Date.beginning_of_week(~D[2020-07-06])
~D[2020-07-06]
iex> Date.beginning_of_week(~D[2020-07-11], :sunday)
~D[2020-07-05]
iex> Date.beginning_of_week(~D[2020-07-11], :saturday)
~D[2020-07-11]
iex> Date.beginning_of_week(~N[2020-07-11 01:23:45])
~D[2020-07-06]
"""
@doc since: "1.11.0"
@spec beginning_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
def beginning_of_week(date, starting_on \\ :default)
def beginning_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
%{year: year, month: month, day: day} = date
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
{year, month, day} =
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
1 ->
{year, month, day}
day_of_week ->
Calendar.ISO.date_from_iso_days(iso_days - day_of_week + 1)
end
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
def beginning_of_week(%{calendar: calendar} = date, starting_on) do
%{year: year, month: month, day: day} = date
case calendar.day_of_week(year, month, day, starting_on) do
{day_of_week, day_of_week, _} ->
%Date{calendar: calendar, year: year, month: month, day: day}
{day_of_week, first_day_of_week, _} ->
add(date, -(day_of_week - first_day_of_week))
end
end
@doc """
Calculates a date that is the last day of the week for the given `date`.
If the day is already the last day of the week, it returns the
day itself. For the built-in ISO calendar, the week ends on Sunday.
A weekday rather than `:default` can be given as `starting_on`.
## Examples
iex> Date.end_of_week(~D[2020-07-11])
~D[2020-07-12]
iex> Date.end_of_week(~D[2020-07-05])
~D[2020-07-05]
iex> Date.end_of_week(~D[2020-07-06], :sunday)
~D[2020-07-11]
iex> Date.end_of_week(~D[2020-07-06], :sunday)
~D[2020-07-11]
iex> Date.end_of_week(~D[2020-07-06], :saturday)
~D[2020-07-10]
iex> Date.end_of_week(~N[2020-07-11 01:23:45])
~D[2020-07-12]
"""
@doc since: "1.11.0"
@spec end_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
def end_of_week(date, starting_on \\ :default)
def end_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
%{year: year, month: month, day: day} = date
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
{year, month, day} =
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
7 ->
{year, month, day}
day_of_week ->
Calendar.ISO.date_from_iso_days(iso_days + 7 - day_of_week)
end
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
def end_of_week(%{calendar: calendar} = date, starting_on) do
%{year: year, month: month, day: day} = date
case calendar.day_of_week(year, month, day, starting_on) do
{day_of_week, _, day_of_week} ->
%Date{calendar: calendar, year: year, month: month, day: day}
{day_of_week, _, last_day_of_week} ->
add(date, last_day_of_week - day_of_week)
end
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.day_of_week(year, month, day)
end
@doc """
@@ -928,45 +739,6 @@ defmodule Date do
calendar.day_of_era(year, month, day)
end
@doc """
Calculates a date that is the first day of the month for the given `date`.
## Examples
iex> Date.beginning_of_month(~D[2000-01-31])
~D[2000-01-01]
iex> Date.beginning_of_month(~D[2000-01-01])
~D[2000-01-01]
iex> Date.beginning_of_month(~N[2000-01-31 01:23:45])
~D[2000-01-01]
"""
@doc since: "1.11.0"
@spec beginning_of_month(Calendar.date()) :: t()
def beginning_of_month(%{year: year, month: month, calendar: calendar}) do
%Date{year: year, month: month, day: 1, calendar: calendar}
end
@doc """
Calculates a date that is the last day of the month for the given `date`.
## Examples
iex> Date.end_of_month(~D[2000-01-01])
~D[2000-01-31]
iex> Date.end_of_month(~D[2000-01-31])
~D[2000-01-31]
iex> Date.end_of_month(~N[2000-01-01 01:23:45])
~D[2000-01-31]
"""
@doc since: "1.11.0"
@spec end_of_month(Calendar.date()) :: t()
def end_of_month(%{year: year, month: month, calendar: calendar} = date) do
day = Date.days_in_month(date)
%Date{year: year, month: month, day: day, calendar: calendar}
end
## Helpers
defimpl String.Chars do
+7 -271
View File
@@ -25,14 +25,10 @@ defmodule DateTime do
datetimes and returns `{:error, :utc_only_time_zone_database}`
for any other time zone.
Other time zone databases can also be configured. For example,
two of the available options are:
* [`tz`](https://hexdocs.pm/tz/)
* [`tzdata`](https://hexdocs.pm/tzdata/)
To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration:
Other time zone databases can also be configured. For example, to use the
[tzdata](https://hexdocs.pm/tzdata/) database, 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, Tzdata.TimeZoneDatabase
@@ -40,7 +36,6 @@ defmodule DateTime do
Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)
See the proper names in the library installation instructions.
"""
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
@@ -77,7 +72,6 @@ defmodule DateTime do
}
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1})
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current datetime in UTC.
@@ -94,165 +88,12 @@ defmodule DateTime do
System.os_time() |> from_unix!(:native, calendar)
end
@doc """
Builds a datetime from date and time structs.
It expects a time zone to put the `DateTime` in.
If the time zone is not passed it will default to `"Etc/UTC"`,
which always succeeds. Otherwise, the `DateTime` is checked against the time zone database
given as `time_zone_database`. See the "Time zone database"
section in the module documentation.
## Examples
iex> DateTime.new(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
{:ok, ~U[2016-05-24 13:26:08.003Z]}
When the datetime is ambiguous - for instance during changing from summer
to winter time - the two possible valid datetimes are returned. First the one
that happens first, then the one that happens after.
iex> {:ambiguous, first_dt, second_dt} = DateTime.new(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> first_dt
#DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen>
iex> second_dt
#DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
When there is a gap in wall time - for instance in spring when the clocks are
turned forward - the latest valid datetime just before the gap and the first
valid datetime just after the gap.
iex> {:gap, just_before, just_after} = DateTime.new(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> just_before
#DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen>
iex> just_after
#DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
Most of the time there is one, and just one, valid datetime for a certain
date and time in a certain time zone.
iex> {:ok, datetime} = DateTime.new(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> datetime
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
"""
@doc since: "1.11.0"
@spec new(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) ::
{:ok, t}
| {:ambiguous, t, t}
| {:gap, t, t}
| {:error,
:incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database}
def new(
date,
time,
time_zone \\ "Etc/UTC",
time_zone_database \\ Calendar.get_time_zone_database()
)
def new(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time, "Etc/UTC", _db) do
%{year: year, month: month, day: day} = date
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
datetime = %DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
{:ok, datetime}
end
def new(date, time, time_zone, time_zone_database) do
with {:ok, naive_datetime} <- NaiveDateTime.new(date, time) do
from_naive(naive_datetime, time_zone, time_zone_database)
end
end
@doc """
Builds a datetime from date and time structs, raising on errors.
It expects a time zone to put the `DateTime` in.
If the time zone is not passed it will default to `"Etc/UTC"`,
which always succeeds. Otherwise, the DateTime is checked against the time zone database
given as `time_zone_database`. See the "Time zone database"
section in the module documentation.
## Examples
iex> DateTime.new!(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
~U[2016-05-24 13:26:08.003Z]
When the datetime is ambiguous - for instance during changing from summer
to winter time - an error will be raised.
iex> DateTime.new!(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
** (ArgumentError) cannot build datetime with ~D[2018-10-28] and ~T[02:30:00] because such instant is ambiguous in time zone Europe/Copenhagen as there is an overlap between #DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen> and #DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
When there is a gap in wall time - for instance in spring when the clocks are
turned forward - an error will be raised.
iex> DateTime.new!(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
** (ArgumentError) cannot build datetime with ~D[2019-03-31] and ~T[02:30:00] because such instant does not exist in time zone Europe/Copenhagen as there is a gap between #DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen> and #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
Most of the time there is one, and just one, valid datetime for a certain
date and time in a certain time zone.
iex> datetime = DateTime.new!(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> datetime
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
"""
@doc since: "1.11.0"
@spec new!(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) :: t
def new!(
date,
time,
time_zone \\ "Etc/UTC",
time_zone_database \\ Calendar.get_time_zone_database()
)
def new!(date, time, time_zone, time_zone_database) do
case new(date, time, time_zone, time_zone_database) do
{:ok, datetime} ->
datetime
{:ambiguous, dt1, dt2} ->
raise ArgumentError,
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
"instant is ambiguous in time zone #{time_zone} as there is an overlap " <>
"between #{inspect(dt1)} and #{inspect(dt2)}"
{:gap, dt1, dt2} ->
raise ArgumentError,
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
"instant does not exist in time zone #{time_zone} as there is a gap " <>
"between #{inspect(dt1)} and #{inspect(dt2)}"
{:error, reason} ->
raise ArgumentError,
"cannot build datetime with #{inspect(date)} and #{inspect(time)}, reason: #{
inspect(reason)
}"
end
end
@doc """
Converts the given Unix time to `DateTime`.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
be converted to microseconds internally. Up to
253402300799 seconds is supported.
be converted to microseconds internally.
Unix times are always in UTC and therefore the DateTime
will be returned in UTC.
@@ -267,26 +108,14 @@ defmodule DateTime do
iex> datetime
~U[2015-05-25 13:26:08.868569Z]
iex> {:ok, datetime} = DateTime.from_unix(253_402_300_799)
iex> datetime
~U[9999-12-31 23:59:59Z]
iex> {:error, :invalid_unix_time} = DateTime.from_unix(253_402_300_800)
The unit can also be an integer as in `t:System.time_unit/0`:
iex> {:ok, datetime} = DateTime.from_unix(143_256_036_886_856, 1024)
iex> datetime
~U[6403-03-17 07:05:22.320312Z]
Negative Unix times are supported up to -377705116800 seconds:
iex> {:ok, datetime} = DateTime.from_unix(-377_705_116_800)
iex> datetime
~U[-9999-01-01 00:00:00Z]
iex> {:error, :invalid_unix_time} = DateTime.from_unix(-377_705_116_801)
Negative Unix times are supported, up to -62167219200 seconds,
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
"""
@spec from_unix(integer, :native | System.time_unit(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
@@ -1038,93 +867,6 @@ defmodule DateTime do
end
end
@doc """
Converts a number of gregorian seconds to a `DateTime` struct.
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
`DateTime.shift_zone/3`.
## Examples
iex> DateTime.from_gregorian_seconds(1)
~U[0000-01-01 00:00:01Z]
iex> DateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
~U[2020-05-01 00:26:31.005Z]
iex> DateTime.from_gregorian_seconds(-1)
~U[-0001-12-31 23:59:59Z]
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days)
%DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
end
@doc """
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
## Examples
iex> dt = %DateTime{year: 0000, month: 1, day: 1, zone_abbr: "UTC",
...> hour: 0, minute: 0, second: 1, microsecond: {0, 0},
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
iex> DateTime.to_gregorian_seconds(dt)
{1, 0}
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "UTC",
...> hour: 0, minute: 26, second: 31, microsecond: {5000, 0},
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
iex> DateTime.to_gregorian_seconds(dt)
{63_755_511_991, 5000}
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "CET",
...> hour: 1, minute: 26, second: 31, microsecond: {5000, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_gregorian_seconds(dt)
{63_755_511_991, 5000}
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(
%{
std_offset: std_offset,
utc_offset: utc_offset,
microsecond: {microsecond, _}
} = datetime
) do
{days, day_fraction} =
datetime
|> to_iso_days()
|> apply_tz_offset(utc_offset + std_offset)
seconds_in_day = seconds_from_day_fraction(day_fraction)
{days * @seconds_per_day + seconds_in_day, microsecond}
end
@doc """
Converts the given `datetime` to a string according to its calendar.
@@ -1525,12 +1267,6 @@ defmodule DateTime do
}
end
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
do: parts_in_day
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
do: div(parts_in_day * @seconds_per_day, parts_per_day)
defimpl String.Chars do
def to_string(datetime) do
%{
+31 -86
View File
@@ -8,12 +8,6 @@ defmodule Calendar.ISO do
applied for all time, consequently the dates give different results
before the year 1583 from when the Gregorian calendar was adopted.
Given this is the default calendar used by Elixir, it has one
difference compared to the ISO8601 specification in that it allows
a whitespace instead of `T` as a seperator between date and times
both when parsing and formatting. Strict formatting can be done
by using the `to_iso8601` found in `NaiveDateTime` and `DateTime`.
Note that while ISO 8601 allows times and datetimes to specify
24:00:00 as the zero hour of the next day, this notation is not
supported by Elixir.
@@ -50,7 +44,6 @@ defmodule Calendar.ISO do
@type hour :: 0..23
@type minute :: 0..59
@type second :: 0..59
@type weekday :: :monday | :tuesday | :wednesday | :thursday | :friday | :saturday | :sunday
@typedoc """
Microseconds with stored precision.
@@ -500,12 +493,12 @@ defmodule Calendar.ISO do
total_microseconds = divide_by_parts_per_day(parts_in_day, parts_per_day)
{hours, rest_microseconds1} =
div_rem(total_microseconds, @seconds_per_hour * @microseconds_per_second)
div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)
{minutes, rest_microseconds2} =
div_rem(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
{seconds, microseconds} = div_rem(rest_microseconds2, @microseconds_per_second)
{seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
{hours, minutes, seconds, {microseconds, 6}}
end
@@ -540,7 +533,7 @@ defmodule Calendar.ISO do
{year, month, day_in_month + 1}
end
defp div_rem(int1, int2) do
defp div_mod(int1, int2) do
div = div(int1, int2)
rem = int1 - div * int2
@@ -626,83 +619,43 @@ defmodule Calendar.ISO do
rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0)
end
# TODO: Deprecate me on v1.15
@doc false
def day_of_week(year, month, day) do
day_of_week(year, month, day, :default) |> elem(0)
end
@doc """
Calculates the day of the week from the given `year`, `month`, and `day`.
It is an integer from 1 to 7, where 1 is the given `starting_on` weekday.
For example, if `starting_on` is set to `:monday`, then 1 is Monday and
7 is Sunday.
`starting_on` can also be `:default`, which is equivalent to `:monday`.
It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.
## Examples
iex> Calendar.ISO.day_of_week(2016, 10, 31, :monday)
{1, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 1, :monday)
{2, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 2, :monday)
{3, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 3, :monday)
{4, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 4, :monday)
{5, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 5, :monday)
{6, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 6, :monday)
{7, 1, 7}
iex> Calendar.ISO.day_of_week(-99, 1, 31, :monday)
{4, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 10, 31, :sunday)
{2, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 1, :sunday)
{3, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 2, :sunday)
{4, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 3, :sunday)
{5, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 4, :sunday)
{6, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 5, :sunday)
{7, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 6, :sunday)
{1, 1, 7}
iex> Calendar.ISO.day_of_week(-99, 1, 31, :sunday)
{5, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 10, 31, :saturday)
{3, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 10, 31)
1
iex> Calendar.ISO.day_of_week(2016, 11, 1)
2
iex> Calendar.ISO.day_of_week(2016, 11, 2)
3
iex> Calendar.ISO.day_of_week(2016, 11, 3)
4
iex> Calendar.ISO.day_of_week(2016, 11, 4)
5
iex> Calendar.ISO.day_of_week(2016, 11, 5)
6
iex> Calendar.ISO.day_of_week(2016, 11, 6)
7
iex> Calendar.ISO.day_of_week(-99, 1, 31)
4
"""
@doc since: "1.11.0"
@spec day_of_week(year, month, day, :default | weekday) :: {day_of_week(), 1, 7}
@doc since: "1.4.0"
@spec day_of_week(year, month, day) :: day_of_week()
@impl true
def day_of_week(year, month, day, starting_on) do
iso_days = date_to_iso_days(year, month, day)
{iso_days_to_day_of_week(iso_days, starting_on), 1, 7}
def day_of_week(year, month, day) do
date_to_iso_days(year, month, day)
|> iso_days_to_day_of_week()
end
@doc false
def iso_days_to_day_of_week(iso_days, starting_on) do
Integer.mod(iso_days + day_of_week_offset(starting_on), 7) + 1
defp iso_days_to_day_of_week(iso_days) do
Integer.mod(iso_days + 5, 7) + 1
end
defp day_of_week_offset(:default), do: 5
defp day_of_week_offset(:wednesday), do: 3
defp day_of_week_offset(:thursday), do: 2
defp day_of_week_offset(:friday), do: 1
defp day_of_week_offset(:saturday), do: 0
defp day_of_week_offset(:sunday), do: 6
defp day_of_week_offset(:monday), do: 5
defp day_of_week_offset(:tuesday), do: 4
@doc """
Calculates the day of the year from the given `year`, `month`, and `day`.
@@ -1207,14 +1160,6 @@ defmodule Calendar.ISO do
end
end
@doc false
def gregorian_seconds_to_iso_days(seconds, microsecond) do
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
microseconds_in_day = rest_seconds * @microseconds_per_second + microsecond
day_fraction = {microseconds_in_day, @parts_per_day}
{days, day_fraction}
end
@doc false
def iso_days_to_unit({days, {parts, ppd}}, unit) do
day_microseconds = days * @parts_per_day
@@ -1367,7 +1312,7 @@ defmodule Calendar.ISO do
end
defp iso_seconds_to_datetime(seconds) do
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
{days, rest_seconds} = div_mod(seconds, @seconds_per_day)
date = date_from_iso_days(days)
time = seconds_to_time(rest_seconds)
@@ -1375,8 +1320,8 @@ defmodule Calendar.ISO do
end
defp seconds_to_time(seconds) when seconds in 0..@last_second_of_the_day do
{hour, rest_seconds} = div_rem(seconds, @seconds_per_hour)
{minute, second} = div_rem(rest_seconds, @seconds_per_minute)
{hour, rest_seconds} = div_mod(seconds, @seconds_per_hour)
{minute, second} = div_mod(rest_seconds, @seconds_per_minute)
{hour, minute, second}
end
-157
View File
@@ -78,8 +78,6 @@ defmodule NaiveDateTime do
microsecond: Calendar.microsecond()
}
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current naive datetime in UTC.
@@ -244,61 +242,6 @@ defmodule NaiveDateTime do
end
end
@doc """
Builds a new ISO naive datetime.
Expects all values to be integers. Returns `naive_datetime`
if each entry fits its appropriate range, raises if
time or date is invalid.
## Examples
iex> NaiveDateTime.new!(2000, 1, 1, 0, 0, 0)
~N[2000-01-01 00:00:00]
iex> NaiveDateTime.new!(2000, 2, 29, 0, 0, 0)
~N[2000-02-29 00:00:00]
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1})
~N[2000-01-01 23:59:59.0]
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, 999_999)
~N[2000-01-01 23:59:59.999999]
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1}, Calendar.ISO)
~N[2000-01-01 23:59:59.0]
iex> NaiveDateTime.new!(2000, 1, 1, 24, 59, 59, 999_999)
** (ArgumentError) cannot build naive datetime, reason: :invalid_time
"""
@doc since: "1.11.0"
@spec new!(
Calendar.year(),
Calendar.month(),
Calendar.day(),
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(
year,
month,
day,
hour,
minute,
second,
microsecond \\ {0, 0},
calendar \\ Calendar.ISO
)
def new!(year, month, day, hour, minute, second, microsecond, calendar) do
case new(year, month, day, hour, minute, second, microsecond, calendar) do
{:ok, naive_datetime} ->
naive_datetime
{:error, reason} ->
raise ArgumentError, "cannot build naive datetime, reason: #{inspect(reason)}"
end
end
@doc """
Builds a naive datetime from date and time structs.
@@ -329,24 +272,6 @@ defmodule NaiveDateTime do
{:ok, naive_datetime}
end
@doc """
Builds a naive datetime from date and time structs.
## Examples
iex> NaiveDateTime.new!(~D[2010-01-13], ~T[23:00:07.005])
~N[2010-01-13 23:00:07.005]
"""
@doc since: "1.11.0"
@spec new!(Date.t(), Time.t()) :: t
def new!(date, time)
def new!(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time) do
{:ok, naive_datetime} = new(date, time)
naive_datetime
end
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
@@ -844,82 +769,6 @@ defmodule NaiveDateTime do
end
end
@doc """
Converts a number of gregorian seconds to a `NaiveDateTime` struct.
## Examples
iex> NaiveDateTime.from_gregorian_seconds(1)
~N[0000-01-01 00:00:01]
iex> NaiveDateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
~N[2020-05-01 00:26:31.005]
iex> NaiveDateTime.from_gregorian_seconds(-1)
~N[-0001-12-31 23:59:59]
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days)
%NaiveDateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}
end
@doc """
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
## Examples
iex> NaiveDateTime.to_gregorian_seconds(~N[0000-01-01 00:00:01])
{1, 0}
iex> NaiveDateTime.to_gregorian_seconds(~N[2020-05-01 00:26:31.005])
{63_755_511_991, 5000}
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
month,
day,
hour,
minute,
second,
{microsecond, precision}
)
seconds_in_day = seconds_from_day_fraction(day_fraction)
{days * @seconds_per_day + seconds_in_day, microsecond}
end
@doc """
Compares two `NaiveDateTime` structs.
@@ -1064,12 +913,6 @@ defmodule NaiveDateTime do
## Helpers
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
do: parts_in_day
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
do: div(parts_in_day * @seconds_per_day, parts_per_day)
# Keep it multiline for proper function clause errors.
defp to_iso_days(%{
calendar: calendar,
+2 -98
View File
@@ -46,7 +46,6 @@ defmodule Time do
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current time in UTC.
@@ -140,44 +139,6 @@ defmodule Time do
end
end
@doc """
Builds a new time.
Expects all values to be integers. Returns `time` if each
entry fits its appropriate range, raises if the time is invalid.
Microseconds can also be given with a precision, which must be an
integer between 0 and 6.
The built-in calendar does not support leap seconds.
## Examples
iex> Time.new!(0, 0, 0, 0)
~T[00:00:00.000000]
iex> Time.new!(23, 59, 59, 999_999)
~T[23:59:59.999999]
iex> Time.new!(24, 59, 59, 999_999)
** (ArgumentError) cannot build time, reason: :invalid_time
"""
@doc since: "1.11.0"
@spec new!(
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case new(hour, minute, second, microsecond, calendar) do
{:ok, time} ->
time
{:error, reason} ->
raise ArgumentError, "cannot build time, reason: #{inspect(reason)}"
end
end
@doc """
Converts the given `time` to a string.
@@ -397,63 +358,6 @@ defmodule Time do
end
end
@doc """
Converts a number of seconds after midnight to a `Time` struct.
## Examples
iex> Time.from_seconds_after_midnight(10_000)
~T[02:46:40]
iex> Time.from_seconds_after_midnight(30_000, {5000, 3})
~T[08:20:00.005]
iex> Time.from_seconds_after_midnight(-1)
~T[23:59:59]
iex> Time.from_seconds_after_midnight(100_000)
~T[03:46:40]
"""
@doc since: "1.11.0"
@spec from_seconds_after_midnight(
integer(),
Calendar.microsecond(),
Calendar.calendar()
) :: t
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
when is_integer(seconds) do
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
{hour, minute, second, {_, _}} =
calendar.time_from_day_fraction({seconds_in_day, @seconds_per_day})
%Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
end
@doc """
Converts a `Time` struct to a number of seconds after midnight.
The returned value is a two-element tuple with the number of seconds and microseconds.
## Examples
iex> Time.to_seconds_after_midnight(~T[23:30:15])
{84615, 0}
iex> Time.to_seconds_after_midnight(~N[2010-04-17 23:30:15.999])
{84615, 999000}
"""
@doc since: "1.11.0"
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
iso_days = {0, to_day_fraction(time)}
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
end
@doc """
Adds the `number` of `unit`s to the given `time`.
@@ -652,12 +556,12 @@ defmodule Time do
As with the `compare/2` function both `Time` structs and other structures
containing time can be used. If for instance a `NaiveDateTime` or `DateTime`
is passed, only the hour, minute, second, and microsecond is considered. Any
is passed, only the hour, month, second, and microsecond is considered. Any
additional information about a date or time zone is ignored when calculating
the difference.
The answer can be returned in any `unit` available from
`t:System.time_unit/0`. If the first time value is earlier than
`t:System.time_unit/0`. If the first unit is smaller than
the second, a negative number is returned.
This function returns the difference in seconds where seconds
@@ -59,7 +59,7 @@ defmodule Calendar.TimeZoneDatabase do
with a limit for when the period ends (wall time). The second nested two-tuple is the period
just after the gap and a datetime (wall time) for when the period begins just after the gap.
If there is only a single possible period for the provided `datetime`, then a tuple with `:ok`
If there is only a single possible period for the provided `datetime`, the a tuple with `:single`
and the `time_zone_period` is returned.
"""
@doc since: "1.8.0"
+27 -90
View File
@@ -86,12 +86,12 @@ defmodule Code do
* `{:local_function, meta, name, arity}` and
`{:local_macro, meta, name, arity}` - traced whenever a local
function or macro is referenced. `meta` is the call AST metadata, followed by
the `name` and `arity`.
function or macro is referenced. `meta` is the call AST metadata, `module`
is the invoked module, followed by the `name` and `arity`.
* `{:compile_env, app, path, return}` - traced whenever `Application.compile_env/3`
or `Application.compile_env!/2` are called. `app` is an atom, `path` is a list
of keys to traverse in the application environment and `return` is either
of keys to traverse in the application environemnt and `return` is either
`{:ok, value}` or `:error`.
The `:tracers` compiler option can be combined with the `:parser_options`
@@ -171,14 +171,14 @@ defmodule Code do
# Now unrequire all files
Code.unrequire_files(Code.required_files())
# Note that modules are still available
# Notice modules are still available
function_exported?(EExTest.Compiled, :before_compile, 0)
#=> true
"""
@doc since: "1.7.0"
@spec unrequire_files([binary]) :: :ok
def unrequire_files(files) when is_list(files) do
def unrequire_files(files) do
:elixir_code_server.cast({:unrequire_files, files})
end
@@ -290,7 +290,7 @@ defmodule Code do
and the second a list of imported macro names and arity; the list
of function names and arity must be sorted
Note that setting any of the values above overrides Elixir's default
Notice that setting any of the values above overrides Elixir's default
values. For example, setting `:requires` to `[]` will no longer
automatically require the `Kernel` module. In the same way setting
`:macros` will no longer auto-import `Kernel` macros like `Kernel.if/2`,
@@ -337,7 +337,7 @@ defmodule Code do
defp eval_string_with_error_handling(string, binding, opts) do
%{line: line, file: file} = env = :elixir.env_for_eval(opts)
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
forms = :elixir.string_to_quoted!(to_charlist(string), line, file, [])
{value, binding, _env} = :elixir.eval_forms(forms, binding, env)
{value, binding}
end
@@ -375,7 +375,7 @@ defmodule Code do
* `:force_do_end_blocks` (since v1.9.0) - when `true`, converts all
inline usages of `do: ...`, `else: ...` and friends into `do/end`
blocks. Defaults to `false`. Note that this option is convergent:
blocks. Defaults to `false`. Notice this option is convergent:
once you set it to `true`, all keywords will be converted. If you
set it to `false` later on, `do/end` blocks won't be converted
back to keywords.
@@ -759,9 +759,6 @@ defmodule Code do
* `:line` - the starting line of the string being parsed.
Defaults to 1.
* `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to 1.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -829,13 +826,12 @@ defmodule Code do
"""
@spec string_to_quoted(List.Chars.t(), keyword) ::
{:ok, Macro.t()} | {:error, {location :: keyword, term, term}}
{:ok, Macro.t()} | {:error, {line :: pos_integer, term, term}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
case :elixir.string_to_tokens(to_charlist(string), line, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
@@ -858,8 +854,7 @@ defmodule Code do
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
:elixir.string_to_quoted!(to_charlist(string), line, column, file, opts)
:elixir.string_to_quoted!(to_charlist(string), line, file, opts)
end
@doc """
@@ -894,7 +889,7 @@ defmodule Code do
If the file was already required, `require_file/2` doesn't do anything and
returns `nil`.
Note that if `require_file/2` is invoked by different processes concurrently,
Notice that if `require_file/2` is invoked by different processes concurrently,
the first process to invoke `require_file/2` acquires a lock and the remaining
ones will block until the file is available. This means that if `require_file/2`
is called more than once with a given file, that file will be compiled only once.
@@ -937,7 +932,7 @@ defmodule Code do
@doc """
Gets all compilation options from the code server.
To get individual options, see `get_compiler_option/1`.
To get invidual options, see `get_compiler_option/1`.
For a description of all options, see `put_compiler_option/2`.
## Examples
@@ -956,7 +951,7 @@ defmodule Code do
@doc """
Stores all given compilation options.
To store individual options, see `put_compiler_option/2`.
To store invidual options, see `put_compiler_option/2`.
For a description of all options, see `put_compiler_option/2`.
## Examples
@@ -968,9 +963,8 @@ defmodule Code do
@spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
def compiler_options(opts) do
for {key, value} <- opts, into: %{} do
previous = get_compiler_option(key)
put_compiler_option(key, value)
{key, previous}
{key, value}
end
end
@@ -1281,7 +1275,7 @@ defmodule Code do
def ensure_compiled(module) when is_atom(module) do
case :code.ensure_loaded(module) do
{:error, :nofile} = error ->
if can_await_module_compilation?() do
if is_pid(:erlang.get(:elixir_compiler_pid)) do
case Kernel.ErrorHandler.ensure_compiled(module, :module, :soft) do
:found -> {:module, module}
:deadlock -> {:error, :unavailable}
@@ -1296,21 +1290,6 @@ defmodule Code do
end
end
@doc """
Returns true if the current process can await for module compilation.
When compiling Elixir code via `Kernel.ParallelCompiler`, which is
used by Mix and `elixirc`, calling a module that has not yet been
compiled will block the caller until the module becomes available.
Executing Elixir scripts, such as passing a filename to `elixir`,
does not await.
"""
@doc since: "1.11.0"
@spec can_await_module_compilation? :: boolean
def can_await_module_compilation? do
:erlang.process_info(self(), :error_handler) == {:error_handler, Kernel.ErrorHandler}
end
@doc false
@deprecated "Use Code.ensure_compiled/1 instead (see the proper disclaimers in its docs)"
def ensure_compiled?(module) when is_atom(module) do
@@ -1347,8 +1326,8 @@ defmodule Code do
docs :: [doc_element]}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | atom(),
doc_content: %{optional(binary) => binary} | :none | :hidden,
beam_language: :elixir | :erlang | :lfe | :alpaca | atom(),
doc_content: %{required(binary) => binary} | :none | :hidden,
doc_element:
{{kind :: atom, function_name :: atom, arity}, annotation, signature, doc_content,
metadata},
@@ -1359,73 +1338,31 @@ defmodule Code do
def fetch_docs(module) when is_atom(module) do
case :code.get_object_code(module) do
{_module, bin, beam_path} ->
case fetch_docs_from_beam(bin) do
{:error, :chunk_not_found} ->
app_root = Path.expand(Path.join(["..", ".."]), beam_path)
path = Path.join([app_root, "doc", "chunks", "#{module}.chunk"])
fetch_docs_from_chunk(path)
other ->
other
end
:error ->
case :code.which(module) do
:preloaded ->
# The erts directory is not necessarily included in releases
# unless it is listed as an extra application.
case :code.lib_dir(:erts) do
path when is_list(path) ->
path = Path.join([path, "doc", "chunks", "#{module}.chunk"])
fetch_docs_from_chunk(path)
{:error, _} ->
{:error, :chunk_not_found}
end
_ ->
{:error, :module_not_found}
end
{_module, bin, _beam_path} -> do_fetch_docs(bin)
:error -> {:error, :module_not_found}
end
end
def fetch_docs(path) when is_binary(path) do
fetch_docs_from_beam(String.to_charlist(path))
do_fetch_docs(String.to_charlist(path))
end
@docs_chunk 'Docs'
defp fetch_docs_from_beam(bin_or_path) do
defp do_fetch_docs(bin_or_path) do
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
{:ok, {_module, [{@docs_chunk, bin}]}} ->
load_docs_chunk(bin)
try do
:erlang.binary_to_term(bin)
rescue
_ -> {:error, {:invalid_chunk, bin}}
end
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} ->
{:error, :chunk_not_found}
{:error, :beam_lib, {:file_error, _, :enoent}} ->
{:error, :module_not_found}
end
end
defp fetch_docs_from_chunk(path) do
case File.read(path) do
{:ok, bin} ->
load_docs_chunk(bin)
{:error, _} ->
{:error, :chunk_not_found}
end
end
defp load_docs_chunk(bin) do
:erlang.binary_to_term(bin)
rescue
_ ->
{:error, {:invalid_chunk, bin}}
end
@doc ~S"""
Deprecated function to retrieve old documentation format.
+7 -13
View File
@@ -42,8 +42,6 @@ defmodule Code.Formatter do
:<~>,
:<|>,
:^^^,
:+++,
:---,
:in,
:++,
:--,
@@ -142,8 +140,8 @@ defmodule Code.Formatter do
Checks if two strings are equivalent.
"""
def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, 1, "nofile", [])
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, 1, "nofile", [])
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, "nofile", [])
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, "nofile", [])
case not_equivalent(quoted1, quoted2) do
{left, right} -> {:error, left, right}
@@ -212,7 +210,7 @@ defmodule Code.Formatter do
token_metadata: true
]
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, 1, file, tokenizer_options),
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, file, tokenizer_options),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, parser_options) do
state =
Process.get(:code_formatter_comments)
@@ -239,8 +237,8 @@ defmodule Code.Formatter do
{:ok, doc} ->
doc
{:error, {location, error, token}} ->
:elixir_errors.parse_error(location, Keyword.get(opts, :file, "nofile"), error, token)
{:error, {line, error, token}} ->
:elixir_errors.parse_error(line, Keyword.get(opts, :file, "nofile"), error, token)
end
end
@@ -485,10 +483,6 @@ defmodule Code.Formatter do
quoted_to_algebra(arg, context, state)
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{"nil", state}
end
defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do
{block, state} = block_to_algebra(block, line(meta), closing_line(meta), state)
{surround("(", block, ")"), state}
@@ -1963,7 +1957,7 @@ defmodule Code.Formatter do
end
# If the document is immediately followed by comment which is followed by newlines,
# its newlines wouldn't have considered the comment, so we need to adjust it.
# its newlines wouldn't have considerd the comment, so we need to adjust it.
defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [{line, _, _} | _])
when newlines > 1 and line == doc_end + 1 do
{doc, next_line, 1}
@@ -2153,7 +2147,7 @@ defmodule Code.Formatter do
end
defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do
meta[:delimiter] != ~s[']
meta[:delimeter] != ~s[']
end
defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do
+1 -1
View File
@@ -44,7 +44,7 @@ defmodule Code.Identifier do
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 170}
op in [:in] -> {:left, 180}
op in [:^^^] -> {:left, 190}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:++, :--, :.., :<>] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
op in [:.] -> {:left, 310}
+6 -6
View File
@@ -273,13 +273,13 @@ defmodule Code.Typespec do
{{:optional, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
end)
case List.keytake(fields, :__struct__, 0) do
{{:__struct__, struct}, fields_pruned} when is_atom(struct) and struct != nil ->
map_pruned = {:%{}, [line: line], fields_pruned}
{:%, [line: line], [struct, map_pruned]}
{struct, fields} = Keyword.pop(fields, :__struct__)
map = {:%{}, [line: line], fields}
_ ->
{:%{}, [line: line], fields}
if struct do
{:%, [line: line], [struct, map]}
else
map
end
end
+53 -115
View File
@@ -13,7 +13,7 @@ defmodule Config do
key1: "value1",
key2: "value2"
import_config "#{config_env()}.exs"
import_config "#{Mix.env()}.exs"
`import Config` will import the functions `config/2`, `config/3`
and `import_config/1` to help you manage your configuration.
@@ -26,9 +26,9 @@ defmodule Config do
"value1" = Application.fetch_env!(:some_app, :key1)
Finally, the line `import_config "#{config_env()}.exs"` will import
other config files based on the current configuration environment,
such as `config/dev.exs` and `config/test.exs`.
Finally, the line `import_config "#{Mix.env()}.exs"` will import other
config files, based on the current Mix environment, such as
`config/dev.exs` and `config/test.exs`.
`Config` also provides a low-level API for evaluating and reading
configuration, under the `Config.Reader` module.
@@ -36,14 +36,14 @@ defmodule Config do
**Important:** 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).
read our [library guidelines](library-guidelines.html).
## Migrating from `use Mix.Config`
The `Config` module in Elixir was introduced in v1.9 as a replacement to
`Mix.Config`, which was specific to Mix and has been deprecated.
You can leverage `Config` instead of `Mix.Config` in three steps. The first
You can leverage `Config` instead of `Mix.Config` in two steps. The first
step is to replace `use Mix.Config` at the top of your config files by
`import Config`.
@@ -59,30 +59,43 @@ defmodule Config do
import_config config
end
The last step is to replace all `Mix.env()` calls by `config_env()`.
## config/releases.exs
## config/runtime.exs
For runtime configuration, you can use the `config/runtime.exs` file.
It is executed right before applications start in both Mix and releases
(assembled with `mix release`).
If you are using releases, see `mix release`, there is another configuration
file called `config/releases.exs`. While `config/config.exs` and friends
mentioned in the previous section are executed whenever you run a Mix
command, including when you assemble a release, `config/releases.exs` is
executed every time your production system boots. Since Mix is not available
in a production system, `config/releases.exs` must not use any of the
functions from Mix.
"""
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports}
@files_key {__MODULE__, :files}
defp get_opts!(), do: Process.get(@opts_key)
defp put_opts(value), do: Process.put(@opts_key, value)
defp delete_opts(), do: Process.delete(@opts_key)
defp get_config!() do
Process.get(@config_key) || raise_improper_use!()
end
defp get_config!(), do: Process.get(@config_key) || raise_improper_use!()
defp put_config(value), do: Process.put(@config_key, value)
defp delete_config(), do: Process.delete(@config_key)
defp put_config(value) do
Process.put(@config_key, value)
end
defp get_imports!(), do: Process.get(@imports_key) || raise_improper_use!()
defp put_imports(value), do: Process.put(@imports_key, value)
defp delete_imports(), do: Process.delete(@imports_key)
defp delete_config() do
Process.delete(@config_key)
end
defp get_files!() do
Process.get(@files_key) || raise_improper_use!()
end
defp put_files(value) do
Process.put(@files_key, value)
end
defp delete_files() do
Process.delete(@files_key)
end
defp raise_improper_use!() do
raise "could not set configuration via Config. " <>
@@ -159,55 +172,6 @@ defmodule Config do
|> put_config()
end
@doc """
Returns the environment this configuration file is executed on.
In Mix projects this function returns the environment this configuration
file is executed on. In releases, the environment when `mix release` ran.
This is most often used to execute conditional code:
if config_env() == :prod do
config :my_app, :debug, false
end
"""
@doc since: "1.11.0"
defmacro config_env() do
quote do
Config.__env__!()
end
end
@doc false
@spec __env__!() :: atom()
def __env__!() do
elem(get_opts!(), 0) || raise "no :env key was given to this configuration file"
end
@doc """
Returns the target this configuration file is executed on.
This is most often used to execute conditional code:
if config_target() == :host do
config :my_app, :debug, false
end
"""
@doc since: "1.11.0"
defmacro config_target() do
quote do
Config.__target__!()
end
end
@doc false
@spec __target__!() :: atom()
def __target__!() do
elem(get_opts!(), 1) || raise "no :target key was given to this configuration file"
end
@doc ~S"""
Imports configuration from the given file.
@@ -220,11 +184,8 @@ defmodule Config do
This is often used to emulate configuration across environments:
import_config "#{config_env()}.exs"
import_config "#{Mix.env()}.exs"
Note, however, some configuration files, such as `config/runtime.exs`
does not support imports, as they are meant to be copied across
systems.
"""
@doc since: "1.9.0"
defmacro import_config(file) do
@@ -237,62 +198,39 @@ defmodule Config do
@doc false
@spec __import__!(Path.t()) :: {term, Code.binding()}
def __import__!(file) when is_binary(file) do
import_config!(file, File.read!(file), true)
current_files = get_files!()
if file in current_files do
raise ArgumentError,
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
end
put_files([file | current_files])
Code.eval_file(file)
end
@doc false
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target)
imports = Keyword.get(opts, :imports, [])
previous_opts = put_opts({env, target})
@spec __eval__!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
def __eval__!(file, imported_paths \\ []) when is_binary(file) and is_list(imported_paths) do
previous_config = put_config([])
previous_imports = put_imports(imports)
previous_files = put_files(imported_paths)
try do
{eval_config, _} = import_config!(file, content, false)
{eval_config, _} = __import__!(Path.expand(file))
case get_config!() do
[] when is_list(eval_config) ->
{validate!(eval_config, file), get_imports!()}
{validate!(eval_config, file), get_files!()}
pdict_config ->
{pdict_config, get_imports!()}
{pdict_config, get_files!()}
end
after
if previous_opts, do: put_opts(previous_opts), else: delete_opts()
if previous_config, do: put_config(previous_config), else: delete_config()
if previous_imports, do: put_imports(previous_imports), else: delete_imports()
if previous_files, do: put_files(previous_files), else: delete_files()
end
end
defp import_config!(file, contents, raise_when_disabled?) do
current_imports = get_imports!()
cond do
current_imports == :disabled ->
if raise_when_disabled? do
raise "import_config/1 is not enabled for this configuration file. " <>
"Some configuration files do not allow importing other files " <>
"as they are often copied to external systems"
end
file in current_imports ->
raise ArgumentError,
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
true ->
put_imports([file | current_imports])
:ok
end
# TODO: Emit a warning if Mix.env() is found in said files in Elixir v1.15.
# Note this won't be a deprecation warning as it will always be emitted.
Code.eval_string(contents, [], file: file)
end
@doc false
def __merge__(config1, config2) when is_list(config1) and is_list(config2) do
Keyword.merge(config1, config2, fn _, app1, app2 ->
+29 -42
View File
@@ -109,13 +109,11 @@ defmodule Config.Provider do
:providers,
:config_path,
extra_config: [],
prune_runtime_sys_config_after_boot: false,
reboot_system_after_config: false,
prune_after_boot: false,
reboot_after_config: true,
validate_compile_env: false
]
@reserved_apps [:kernel, :stdlib]
@doc """
Validates a `t:config_path/0`.
"""
@@ -149,44 +147,44 @@ defmodule Config.Provider do
def resolve_config_path!(path) when is_binary(path), do: path
def resolve_config_path!({:system, name, path}), do: System.fetch_env!(name) <> path
# Private keys
@init_key :config_provider_init
@booted_key :config_provider_booted
# Public keys
@reboot_mode_key :config_provider_reboot_mode
@doc false
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
init = struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
[elixir: [{@init_key, init}]]
struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
end
@doc false
def boot(reboot_fun \\ &restart_and_sleep/0) do
def boot(app, key, restart_fun \\ &restart_and_sleep/0) do
# The app with the config provider settings may not
# have been loaded at this point, so make sure we load
# its environment before querying it.
_ = :application.load(app)
# The config provider typically runs very early in the
# release process, so we need to make sure Elixir is started
# before we go around running Elixir code.
{:ok, _} = :application.ensure_all_started(:elixir)
case Application.fetch_env(:elixir, @booted_key) do
# The key we store if the system already booted
booted_key = :"#{key}_booted"
case :application.get_env(app, booted_key) do
{:ok, {:booted, path}} ->
path && File.rm(path)
with {:ok, %Config.Provider{} = provider} <- Application.fetch_env(:elixir, @init_key) do
with {:ok, %Config.Provider{} = provider} <- :application.get_env(app, key) do
maybe_validate_compile_env(provider)
end
:booted
_ ->
case Application.fetch_env(:elixir, @init_key) do
case :application.get_env(app, key) do
{:ok, %Config.Provider{} = provider} ->
path = resolve_config_path!(provider.config_path)
reboot_config = [elixir: [{@booted_key, booted_value(provider, path)}]]
boot_providers(path, provider, reboot_config, reboot_fun)
reboot_config = [{app, [{booted_key, booted_value(provider, path)}]}]
boot_providers(path, provider, reboot_config, restart_fun)
_ ->
:skip
@@ -194,8 +192,9 @@ defmodule Config.Provider do
end
end
defp boot_providers(path, provider, reboot_config, reboot_fun) do
defp boot_providers(path, provider, reboot_config, restart_fun) do
validate_no_cyclic_boot!(path)
loaded_applications = :application.loaded_applications()
original_config = read_config!(path)
config =
@@ -203,16 +202,16 @@ defmodule Config.Provider do
|> Config.__merge__(provider.extra_config)
|> run_providers(provider)
if provider.reboot_system_after_config do
if provider.reboot_after_config do
config
|> Config.__merge__(reboot_config)
|> write_config!(path)
reboot_fun.()
restart_fun.()
else
for app <- @reserved_apps, config[app] != original_config[app] do
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
abort("""
Cannot configure #{inspect(app)} because :reboot_system_after_config has been set \
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
to false and #{inspect(app)} has already been loaded, meaning any further \
configuration won't have an effect.
@@ -239,9 +238,9 @@ defmodule Config.Provider do
end
@doc false
def validate_compile_env(compile_env, ensure_loaded? \\ true) do
def validate_compile_env(compile_env) do
for {app, [key | path], compile_return} <- compile_env,
ensure_app_loaded?(app, ensure_loaded?) do
Application.ensure_loaded(app) == :ok do
try do
traverse_env(Application.fetch_env(app, key), path)
rescue
@@ -276,9 +275,6 @@ defmodule Config.Provider do
:ok
end
defp ensure_app_loaded?(app, true), do: Application.ensure_loaded(app) == :ok
defp ensure_app_loaded?(app, false), do: Application.spec(app, :vsn) != nil
defp path(key, []), do: "for key #{inspect(key)}"
defp path(key, path), do: "for path #{inspect(path)} inside key #{inspect(key)}"
@@ -303,22 +299,13 @@ defmodule Config.Provider do
defp traverse_env(:error, _paths), do: :error
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
@compile {:no_warn_undefined, {:init, :restart, 1}}
defp restart_and_sleep() do
mode = Application.get_env(:elixir, @reboot_mode_key)
# TODO: Remove otp_release check once we require Erlang/OTP 23+
if :erlang.system_info(:otp_release) >= '23' and mode in [:embedded, :interactive] do
:init.restart(mode: mode)
else
:init.restart()
end
defp restart_and_sleep do
:init.restart()
Process.sleep(:infinity)
end
defp booted_value(%{prune_runtime_sys_config_after_boot: true}, path), do: {:booted, path}
defp booted_value(%{prune_runtime_sys_config_after_boot: false}, _path), do: {:booted, nil}
defp booted_value(%{prune_after_boot: true}, path), do: {:booted, path}
defp booted_value(%{prune_after_boot: false}, _path), do: {:booted, nil}
defp validate_no_cyclic_boot!(path) do
if System.get_env("ELIXIR_CONFIG_PROVIDER_BOOTED") do
+24 -56
View File
@@ -18,18 +18,8 @@ defmodule Config.Reader do
config_provider: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
You can also pass a keyword list of options to the reader,
where the `:path` is a required key:
config_providers: [
{Config.Reader,
path: "/etc/config.exs",
env: :prod,
imports: :disabled}
]
Note by default Mix releases supports runtime configuration via
a `config/runtime.exs`. If a `config/runtime.exs` exists in your
a `config/releases.exs`. If a `config/releases.exs` exists in your
application, it is automatically copied inside the release and
automatically set as a config provider.
"""
@@ -37,71 +27,49 @@ defmodule Config.Reader do
@behaviour Config.Provider
@impl true
def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path)
Config.Provider.validate_config_path!(path)
{path, opts}
end
def init(path) do
init(path: path)
Config.Provider.validate_config_path!(path)
path
end
@impl true
def load(config, {path, opts}) do
merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
end
@doc """
Evaluates the configuration `contents` for the given `file`.
Accepts the same options as `read!/2`.
"""
@doc since: "1.11.0"
@spec eval!(Path.t(), binary, keyword) :: keyword
def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
def load(config, path) do
merge(config, path |> Config.Provider.resolve_config_path!() |> read!())
end
@doc """
Reads the configuration file.
## Options
The same as `read_imports!/2` but only returns the configuration
in the given file, without returning the imported paths.
* `:imports` - a list of already imported paths or `:disabled`
to disable imports
It exists for convenience purposes. For example, you could
invoke it inside your `mix.exs` to read some external data
you decided to move to a configuration file:
* `:env` - the environment the configuration file runs on.
See `Config.config_env/0` for sample usage
* `:target` - the target the configuration file runs on.
See `Config.config_target/0` for sample usage
releases: Config.Reader.read!("rel/releases.exs")
"""
@doc since: "1.9.0"
@spec read!(Path.t(), keyword) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@spec read!(Path.t(), [Path.t()]) :: keyword
def read!(file, imported_paths \\ [])
when is_binary(file) and is_list(imported_paths) do
Config.__eval__!(file, imported_paths) |> elem(0)
end
@doc """
Reads the given configuration file and returns the configuration
with its imports.
Reads the given configuration file alongside its imports.
Accepts the same options as `read!/2`. Although note the `:imports`
option cannot be disabled in `read_imports!/2`.
It accepts a list of `imported_paths` that should raise if attempted
to be imported again (to avoid recursive imports).
It returns a tuple with the configuration and the imported paths.
"""
@doc since: "1.9.0"
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths"
end
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts)
@spec read_imports!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
def read_imports!(file, imported_paths \\ [])
when is_binary(file) and is_list(imported_paths) do
Config.__eval__!(file, imported_paths)
end
@doc """
+5 -8
View File
@@ -18,14 +18,11 @@ defmodule Dict do
message =
"Use the Map module for working with maps or the Keyword module for working with keyword lists"
@deprecated message
defmacro __using__(_) do
# Use this import to guarantee proper code expansion
import Kernel, except: [size: 1]
if __CALLER__.module != HashDict do
IO.warn("use Dict is deprecated. " <> unquote(message), Macro.Env.stacktrace(__CALLER__))
end
quote do
message = "Use maps and the Map module instead"
@@ -155,13 +152,13 @@ defmodule Dict do
end
@deprecated message
def update(dict, key, default, fun) do
def update(dict, key, initial, fun) do
case fetch(dict, key) do
{:ok, value} ->
put(dict, key, fun.(value))
:error ->
put(dict, key, default)
put(dict, key, initial)
end
end
@@ -378,8 +375,8 @@ defmodule Dict do
@deprecated message
@spec update(t, key, value, (value -> value)) :: t
def update(dict, key, default, fun) do
target(dict).update(dict, key, default, fun)
def update(dict, key, initial, fun) do
target(dict).update(dict, key, initial, fun)
end
@deprecated message
+1 -2
View File
@@ -418,8 +418,7 @@ defmodule DynamicSupervisor do
"""
@doc since: "1.6.0"
@spec which_children(Supervisor.supervisor()) :: [
# module() | :dynamic here because :supervisor.modules() is not exported
{:undefined, pid | :restarting, :worker | :supervisor, module() | :dynamic}
{:undefined, pid | :restarting, :worker | :supervisor, :supervisor.modules()}
]
def which_children(supervisor) do
call(supervisor, :which_children)
+39 -117
View File
@@ -20,7 +20,7 @@ defprotocol Enumerable do
Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
end
Note that the user-supplied function is wrapped into a `t:reducer/0` function.
Notice the user-supplied function is wrapped into a `t:reducer/0` function.
The `t:reducer/0` function must return a tagged tuple after each step,
as described in the `t:acc/0` type. At the end, `Enumerable.reduce/3`
returns `t:result/0`.
@@ -161,9 +161,6 @@ defprotocol Enumerable do
Otherwise it should return `{:error, __MODULE__}` and a default algorithm
built on top of `reduce/3` that runs in linear time will be used.
When not called whithin guards, the [`in`](`in/2`) and [`not in`](`in/2`)
operators work by using this function.
"""
@spec member?(t, term) :: {:ok, boolean} | {:error, module}
def member?(enumerable, element)
@@ -1337,12 +1334,12 @@ defmodule Enum do
end
@doc """
Joins the given `enumerable` into a string using `joiner` as a
Joins the given `enumerable` into a binary using `joiner` as a
separator.
If `joiner` is not passed at all, it defaults to an empty string.
If `joiner` is not passed at all, it defaults to the empty binary.
All elements in the `enumerable` must be convertible to a string,
All elements in the `enumerable` must be convertible to a binary,
otherwise an error is raised.
## Examples
@@ -1476,10 +1473,12 @@ defmodule Enum do
@doc """
Maps and joins the given `enumerable` in one pass.
If `joiner` is not passed at all, it defaults to an empty string.
`joiner` can be either a binary or a list and the result will be of
the same type as `joiner`.
If `joiner` is not passed at all, it defaults to an empty binary.
All elements returned from invoking the `mapper` must be convertible to
a string, otherwise an error is raised.
a binary, otherwise an error is raised.
## Examples
@@ -1532,7 +1531,6 @@ defmodule Enum do
end
@doc false
@spec max(t, (() -> empty_result)) :: element | empty_result when empty_result: any
def max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
max(enumerable, &>=/2, empty_fallback)
end
@@ -1564,8 +1562,8 @@ defmodule Enum do
In the example above, `max/2` returned March 31st instead of April 1st
because the structural comparison compares the day before the year.
For this reason, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1579,9 +1577,7 @@ defmodule Enum do
0
"""
@spec max(t, (element, element -> boolean) | module()) ::
element | empty_result
when empty_result: any
@spec max(t, (() -> empty_result)) :: element | empty_result when empty_result: any
@spec max(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
element | empty_result
when empty_result: any
@@ -1593,12 +1589,6 @@ defmodule Enum do
defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)
@doc false
@spec max_by(
t,
(element -> any),
(() -> empty_result) | (element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
def max_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
max_by(enumerable, fun, &>=/2, empty_fallback)
@@ -1628,8 +1618,8 @@ defmodule Enum do
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1648,6 +1638,8 @@ defmodule Enum do
nil
"""
@spec max_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
when empty_result: any
@spec max_by(
t,
(element -> any),
@@ -1680,9 +1672,6 @@ defmodule Enum do
iex> Enum.member?([:a, :b, :c], :d)
false
When not called whithin guards, the [`in`](`in/2`) and [`not in`](`in/2`)
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) when is_list(enumerable) do
@@ -1704,7 +1693,6 @@ defmodule Enum do
end
@doc false
@spec min(t, (() -> empty_result)) :: element | empty_result when empty_result: any
def min(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
min(enumerable, &<=/2, empty_fallback)
end
@@ -1736,8 +1724,8 @@ defmodule Enum do
In the example above, `min/2` returned April 1st instead of March 31st
because the structural comparison compares the day before the year.
For this reason, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1751,9 +1739,7 @@ defmodule Enum do
0
"""
@spec min(t, (element, element -> boolean) | module()) ::
element | empty_result
when empty_result: any
@spec min(t, (() -> empty_result)) :: element | empty_result when empty_result: any
@spec min(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
element | empty_result
when empty_result: any
@@ -1765,12 +1751,6 @@ defmodule Enum do
defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)
@doc false
@spec min_by(
t,
(element -> any),
(() -> empty_result) | (element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
def min_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
min_by(enumerable, fun, &<=/2, empty_fallback)
@@ -1800,8 +1780,8 @@ defmodule Enum do
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1820,6 +1800,8 @@ defmodule Enum do
nil
"""
@spec min_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
when empty_result: any
@spec min_by(
t,
(element -> any),
@@ -1872,14 +1854,6 @@ defmodule Enum do
end
end
@doc false
@spec min_max_by(t, (element -> any), (() -> empty_result)) :: {element, element} | empty_result
when empty_result: any
def min_max_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, &</2, empty_fallback)
end
@doc """
Returns a tuple with the minimal and the maximal elements in the
enumerable as calculated by the given function.
@@ -1887,6 +1861,9 @@ defmodule Enum do
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
Calls the provided `empty_fallback` function and returns its value if
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
## Examples
iex> Enum.min_max_by(["aaa", "bb", "c"], fn x -> String.length(x) end)
@@ -1898,56 +1875,11 @@ defmodule Enum do
iex> Enum.min_max_by([], &String.length/1, fn -> {nil, nil} end)
{nil, nil}
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
iex> users = [
...> %{name: "Ellis", birthday: ~D[1943-05-11]},
...> %{name: "Lovelace", birthday: ~D[1815-12-10]},
...> %{name: "Turing", birthday: ~D[1912-06-23]}
...> ]
iex> Enum.min_max_by(users, &(&1.birthday), Date)
{
%{name: "Lovelace", birthday: ~D[1815-12-10]},
%{name: "Ellis", birthday: ~D[1943-05-11]}
}
Finally, if you don't want to raise on empty enumerables, you can pass
the empty fallback:
iex> Enum.min_max_by([], &String.length/1, fn -> nil end)
nil
"""
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
{element, element} | empty_result
@spec min_max_by(t, (element -> any), (() -> empty_result)) :: {element, element} | empty_result
when empty_result: any
@spec min_max_by(
t,
(element -> any),
(element, element -> boolean) | module(),
(() -> empty_result)
) :: {element, element} | empty_result
when empty_result: any
def min_max_by(
enumerable,
fun,
sorter_or_empty_fallback \\ &</2,
empty_fallback \\ fn -> raise Enum.EmptyError end
)
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
end
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_function(sorter, 2) and is_function(empty_fallback, 0) do
def min_max_by(enumerable, fun, empty_fallback \\ fn -> raise Enum.EmptyError end)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
first_fun = fn entry ->
fun_entry = fun.(entry)
{entry, entry, fun_entry, fun_entry}
@@ -1957,10 +1889,10 @@ defmodule Enum do
fun_entry = fun.(entry)
cond do
sorter.(fun_entry, fun_min) ->
fun_entry < fun_min ->
{entry, prev_max, fun_entry, fun_max}
sorter.(fun_max, fun_entry) ->
fun_entry > fun_max ->
{prev_min, entry, fun_min, fun_entry}
true ->
@@ -1974,8 +1906,6 @@ defmodule Enum do
end
end
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
@doc """
Splits the `enumerable` in two lists according to the given function `fun`.
@@ -2176,7 +2106,7 @@ defmodule Enum do
operation cannot be expressed by any of the functions in the `Enum`
module, developers will most likely resort to `reduce/3`.
"""
@spec reduce(t, any, (element, acc -> acc)) :: acc
@spec reduce(t, any, (element, any -> any)) :: any
def reduce(enumerable, acc, fun) when is_list(enumerable) do
:lists.foldl(fun, acc, enumerable)
end
@@ -2564,15 +2494,15 @@ defmodule Enum do
iex> Enum.sort(dates)
[~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]
Note that the returned result is incorrect, because `sort/1` by default uses
Notice the returned result is incorrect, because `sort/1` by default uses
`<=/2`, which will compare their structure. When comparing structures, the
fields are compared in alphabetical order, which means the dates above will
be compared by `day`, `month` and then `year`, which is the opposite of what
we want.
For this reason, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greather than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -2908,7 +2838,7 @@ defmodule Enum do
@doc """
Takes `count` random elements from `enumerable`.
Note that this function will traverse the whole `enumerable` to
Notice this function will traverse the whole `enumerable` to
get the random sublist.
See `random/1` for notes on implementation and random seed.
@@ -3150,18 +3080,10 @@ defmodule Enum do
"""
@spec with_index(t, integer) :: [{element, index}]
def with_index(enumerable, offset \\ 0) do
enumerable
|> to_list()
|> do_with_index(offset)
end
@spec do_with_index(list, integer) :: [{element, index}]
defp do_with_index([], _) do
[]
end
defp do_with_index([head | tail], index) do
[{head, index} | do_with_index(tail, index + 1)]
map_reduce(enumerable, offset, fn x, acc ->
{{x, acc}, acc + 1}
end)
|> elem(0)
end
@doc """
+47 -74
View File
@@ -21,15 +21,15 @@ defmodule Exception do
@typedoc "The kind handled by formatting functions"
@type kind :: :error | non_error_kind
@type non_error_kind :: :exit | :throw | {:EXIT, pid}
@typep non_error_kind :: :exit | :throw | {:EXIT, pid}
@type stacktrace :: [stacktrace_entry]
@type stacktrace_entry ::
{module, atom, arity_or_args, location}
| {(... -> any), arity_or_args, location}
@type arity_or_args :: non_neg_integer | list
@type location :: keyword
@typep arity_or_args :: non_neg_integer | list
@typep location :: keyword
@callback exception(term) :: t
@callback message(t) :: String.t()
@@ -46,8 +46,6 @@ defmodule Exception do
@doc """
Returns `true` if the given `term` is an exception.
"""
# TODO: Remove this on Elixir v1.15
@doc deprecated: "Use Kernel.is_exception/1 instead"
def exception?(term)
def exception?(%_{__exception__: true}), do: true
def exception?(_), do: false
@@ -190,7 +188,7 @@ defmodule Exception do
Where `definition` is `:def`, `:defp`, `:defmacro` or `:defmacrop`.
"""
@doc since: "1.5.0"
@spec blame_mfa(module, function :: atom, args :: [term]) ::
@spec blame_mfa(module, function, args :: [term]) ::
{:ok, :def | :defp | :defmacro | :defmacrop, [{args :: [term], guards :: [term]}]}
| :error
def blame_mfa(module, function, args)
@@ -239,11 +237,7 @@ defmodule Exception do
binding = :orddict.store(:VAR, call_arg, binding)
try do
ann = :erl_anno.new(0)
{:value, _, binding} =
:erl_eval.expr({:match, ann, erl_arg, {:var, ann, :VAR}}, binding, :none)
{:value, _, binding} = :erl_eval.expr({:match, 0, erl_arg, {:var, 0, :VAR}}, binding, :none)
{true, binding}
rescue
_ -> {false, binding}
@@ -267,13 +261,7 @@ defmodule Exception do
blame_guard(right, scope, binding)
]
kernel_op =
case op do
:orelse -> :or
:andalso -> :and
end
{kernel_op, meta, guards}
{rewrite_guard_call(op), meta, guards}
end
defp blame_guard(ex_guard, scope, binding) do
@@ -292,17 +280,32 @@ defmodule Exception do
defp rewrite_guard(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
{{:., _, [:erlang, :element]}, _, [{{:., _, [:erlang, :+]}, _, [int, 1]}, arg]} ->
{:elem, [], [arg, int]}
{{:., _, [:erlang, :element]}, _, [int, arg]} when is_integer(int) ->
{:elem, [], [arg, int - 1]}
{:., _, [:erlang, call]} ->
rewrite_guard_call(call)
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}
end
end
defp rewrite_guard_call(:orelse), do: :or
defp rewrite_guard_call(:andalso), do: :and
defp rewrite_guard_call(:"=<"), do: :<=
defp rewrite_guard_call(:"/="), do: :!=
defp rewrite_guard_call(:"=:="), do: :===
defp rewrite_guard_call(:"=/="), do: :!==
defp rewrite_guard_call(op) when op in [:band, :bor, :bnot, :bsl, :bsr, :bxor],
do: {:., [], [Bitwise, op]}
defp rewrite_guard_call(op) when op in [:xor, :element, :size], do: {:., [], [:erlang, op]}
defp rewrite_guard_call(op), do: op
defp blame_wrap(match?, ast), do: %{match?: match?, node: ast}
@@ -635,7 +638,6 @@ defmodule Exception do
@doc """
Formats the given `file` and `line` as shown in stacktraces.
If any of the values are `nil`, they are omitted.
## Examples
@@ -651,42 +653,14 @@ defmodule Exception do
"""
def format_file_line(file, line, suffix \\ "") do
cond do
is_nil(file) -> ""
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
true -> "#{file}:#{line}:#{suffix}"
end
end
@doc """
Formats the given `file`, `line`, and `column` as shown in stacktraces.
If any of the values are `nil`, they are omitted.
## Examples
iex> Exception.format_file_line_column("foo", 1, 2)
"foo:1:2:"
iex> Exception.format_file_line_column("foo", 1, nil)
"foo:1:"
iex> Exception.format_file_line_column("foo", nil, nil)
"foo:"
iex> Exception.format_file_line_column("foo", nil, 2)
"foo:"
iex> Exception.format_file_line_column(nil, nil, nil)
if file do
if line && line != 0 do
"#{file}:#{line}:#{suffix}"
else
"#{file}:#{suffix}"
end
else
""
"""
def format_file_line_column(file, line, column, suffix \\ "") do
cond do
is_nil(file) -> ""
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
is_nil(column) or column == 0 -> "#{file}:#{line}:#{suffix}"
true -> "#{file}:#{line}:#{column}:#{suffix}"
end
end
@@ -718,7 +692,7 @@ defmodule ArgumentError do
not is_atom(module) and is_atom(function) and args == [] ->
"you attempted to apply #{inspect(function)} on #{inspect(module)}. " <>
"If you are using apply/3, make sure the module is an atom. " <>
"If you are using the dot syntax, such as map.field or module.function(), " <>
"If you are using the dot syntax, such as map.field or module.function, " <>
"make sure the left side of the dot is an atom or a map"
not is_atom(module) ->
@@ -792,22 +766,21 @@ defmodule SystemLimitError do
end
defmodule SyntaxError do
defexception [:file, :line, :column, description: "syntax error"]
defexception [:file, :line, description: "syntax error"]
@impl true
def message(%{file: file, line: line, column: column, description: description}) do
Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <>
" " <> description
def message(exception) do
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
" " <> exception.description
end
end
defmodule TokenMissingError do
defexception [:file, :line, :column, description: "expression is incomplete"]
defexception [:file, :line, description: "expression is incomplete"]
@impl true
def message(%{file: file, line: line, column: column, description: description}) do
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column) <>
" " <> description
def message(%{file: file, line: line, description: description}) do
Exception.format_file_line(file && Path.relative_to_cwd(file), line) <> " " <> description
end
end
@@ -977,7 +950,7 @@ defmodule UndefinedFunctionError do
end
defp hint(nil, _function, 0, _loaded?) do
". If you are using the dot syntax, such as map.field or module.function(), " <>
". If you are using the dot syntax, such as map.field or module.function, " <>
"make sure the left side of the dot is an atom or a map"
end
@@ -1356,7 +1329,7 @@ defmodule File.CopyError do
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
location =
case exception.on do
case exception.on() do
"" -> ""
on -> ". #{on}"
end
@@ -1374,7 +1347,7 @@ defmodule File.RenameError do
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
location =
case exception.on do
case exception.on() do
"" -> ""
on -> ". #{on}"
end
+1 -9
View File
@@ -110,7 +110,6 @@ defmodule File do
@type stream_mode ::
encoding_mode()
| :append
| :trim_bom
| {:read_ahead, pos_integer | false}
| {:delayed_write, non_neg_integer, non_neg_integer}
@@ -736,7 +735,6 @@ defmodule File do
File.rename("samples", "tmp")
"""
@doc since: "1.1.0"
@spec rename(Path.t(), Path.t()) :: :ok | {:error, posix}
def rename(source, destination) do
:file.rename(source, destination)
@@ -1505,12 +1503,6 @@ defmodule File do
@doc """
Sets the current working directory.
The current working directory is set for the BEAM globally. This can lead to
race conditions if multiple processes are changing the current working
directory concurrently. To run an external command in a given directory
without changing the global current working directory, use the `:cd` option
of `System.cmd/3` and `Port.open/2`.
Returns `:ok` if successful, `{:error, reason}` otherwise.
"""
@spec cd(Path.t()) :: :ok | {:error, posix}
@@ -1645,7 +1637,7 @@ defmodule File do
See `Stream.run/1` for an example of streaming into a file.
"""
@spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t()
@spec stream!(Path.t(), stream_mode, :line | pos_integer) :: File.Stream.t()
def stream!(path, modes \\ [], line_or_bytes \\ :line) do
modes = normalize_modes(modes, true)
File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes)
+4 -20
View File
@@ -8,21 +8,8 @@ defmodule Function do
iex> add.(1, 2)
3
Anonymous functions can also have multiple clauses. All clauses
should expect the same number of arguments:
iex> negate = fn
...> true -> false
...> false -> true
...> end
iex> negate.(false)
true
## The capture operator
It is also possible to capture public module functions and pass them
around as if they were anonymous functions by using the capture
operator `Kernel.SpecialForms.&/1`:
It is also possible to capture module functions and pass them around
as if they were anonymous functions by using the capture operator `&/1`:
iex> add = &Kernel.+/2
iex> add.(1, 2)
@@ -32,9 +19,8 @@ defmodule Function do
iex> length.("hello")
5
To capture a definition within the current module, you can skip the
module prefix, such as `&my_fun/2`. In those cases, the captured
function can be public (`def`) or private (`defp`).
It is also possible to capture a definition in the current module by
skipping the module prefix, such as `&my_fun/2`.
The capture operator can also be used to create anonymous functions
that expect at least one argument:
@@ -45,8 +31,6 @@ defmodule Function do
In such cases, using the capture operator is no different than using `fn`.
## Internal and external functions
We say that functions that point to definitions residing in modules, such
as `&String.length/1`, are **external** functions. All other functions are
**local** and they are always bound to the file or module that defined them.
+1 -1
View File
@@ -7,7 +7,7 @@ defmodule GenEvent do
If you are interested in implementing an event manager, please read the
"Alternatives" section below. If you have to implement an event handler to
integrate with an existing system, such as Elixir's Logger, please use
[`:gen_event`](https://erlang.org/doc/man/gen_event.html) instead.
`:gen_event` instead.
## Alternatives
+2 -9
View File
@@ -280,10 +280,6 @@ defmodule GenServer do
GenServer.call(__MODULE__, {:add, a, b})
end
def subtract(a, b) do
GenServer.call(__MODULE__, {:subtract, a, b})
end
def handle_call({:add, a, b}, _from, state) do
{:reply, a + b, state}
end
@@ -1181,11 +1177,8 @@ defmodule GenServer do
end
@doc """
Returns the `pid` or `{name, node}` of a GenServer process, `nil` otherwise.
To be precise, `nil` is returned whenever a `pid` or `{name, node}` cannot
be returned. Note there is no guarantee the returned `pid` or `{name, node}`
is alive, as a process could terminate immediately after it is looked up.
Returns the `pid` or `{name, node}` of a GenServer process, or `nil` if
no process is associated with the given `server`.
## Examples
+6 -6
View File
@@ -48,8 +48,8 @@ defmodule HashDict do
end
@deprecated message
def update(%HashDict{root: root, size: size}, key, default, fun) when is_function(fun, 1) do
{root, counter} = do_update(root, key, fn -> default end, fun, key_hash(key))
def update(%HashDict{root: root, size: size}, key, initial, fun) when is_function(fun, 1) do
{root, counter} = do_update(root, key, fn -> initial end, fun, key_hash(key))
%HashDict{root: root, size: size + counter}
end
@@ -135,25 +135,25 @@ defmodule HashDict do
end
end
defp do_update(node, key, default, fun, hash) do
defp do_update(node, key, initial, fun, hash) do
index = key_mask(hash)
case elem(node, index) do
[] ->
{put_elem(node, index, [key | default.()]), 1}
{put_elem(node, index, [key | initial.()]), 1}
[^key | value] ->
{put_elem(node, index, [key | fun.(value)]), 0}
[k | v] ->
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | default.()])
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | initial.()])
{put_elem(node, index, {k, v, n}), 1}
{^key, value, n} ->
{put_elem(node, index, {key, fun.(value), n}), 0}
{k, v, n} ->
{n, counter} = do_update(n, key, default, fun, key_shift(hash))
{n, counter} = do_update(n, key, initial, fun, key_shift(hash))
{put_elem(node, index, {k, v, n}), counter}
end
end
+9 -11
View File
@@ -31,17 +31,15 @@ defprotocol Inspect do
end
end
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from
`Inspect.Algebra` and it concatenates algebra documents together.
In the example above it is concatenating the string `"#MapSet<"`,
the document returned by `Inspect.Algebra.to_doc/2`, and the final
string `">"`. We prefix the module name `#` to denote the inspect
presentation is not actually valid Elixir syntax.
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from `Inspect.Algebra` and it
concatenates algebra documents together. In the example above it is
concatenating the string `"MapSet<"`, the document returned by
`Inspect.Algebra.to_doc/2`, and the final string `">"`. All strings are
valid algebra documents that keep their formatting when pretty printed.
Finally, note strings themselves are valid algebra documents that
keep their formatting when pretty printed. This means your `Inspect`
implementation may simply return a string, although that will devoid
it of any pretty-printing.
Since regular strings are valid entities in an algebra document,
an implementation of the `Inspect` protocol may simply return a
string, although that will devoid it of any pretty-printing.
## Error handling
@@ -439,7 +437,7 @@ defimpl Inspect, for: Any do
def inspect(%module{} = struct, opts) do
try do
module.__struct__()
module.__struct__
rescue
_ -> Inspect.Map.inspect(struct, opts)
else
+22 -35
View File
@@ -31,24 +31,22 @@ defmodule Inspect.Opts do
options. Useful when implementing the `Inspect` protocol for nested structs
to pass the custom options through.
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents.
Defaults to `Inspect.inspect/2`.
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents,
defaults to `Inspect.inspect/2`.
* `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option.
bitstrings, maps, lists and any other collection of items. It does not
apply to printable strings nor printable charlists and defaults to 50.
If you don't want to limit the number of items to a particular number,
use `:infinity`. It accepts a positive integer or `:infinity`.
Defaults to `50`.
use `:infinity`.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
* `:pretty` - if set to `true` enables pretty printing, defaults to `false`.
* `:printable_limit` - limits the number of characters that are inspected
on printable strings and printable charlists. You can use `String.printable?/1`
and `List.ascii_printable?/1` to check if a given string or charlist is
printable. If you don't want to limit the number of characters to a particular
number, use `:infinity`. It accepts a positive integer or `:infinity`.
Defaults to `4096`.
printable. Defaults to 4096. If you don't want to limit the number of
characters to a particular number, use `:infinity`.
* `:safe` - when `false`, failures while inspecting structs will be raised
as errors instead of being wrapped in the `Inspect.Error` exception. This
@@ -56,7 +54,7 @@ defmodule Inspect.Opts do
implementations.
* `:structs` - when `false`, structs are not formatted by the inspect
protocol, they are instead printed as maps. Defaults to `true`.
protocol, they are instead printed as maps, defaults to `true`.
* `:syntax_colors` - when set to a keyword list of colors the output is
colorized. The keys are types and the values are the colors to use for
@@ -65,10 +63,10 @@ defmodule Inspect.Opts do
`:string`, and `:tuple`. Custom data types may provide their own options.
Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.
* `:width` - number of characters per line used when pretty is `true` or when
printing to IO devices. Set to `0` to force each item to be printed on its
* `:width` - defaults to 80 characters, used when pretty is `true` or when
printing to IO devices. Set to 0 to force each item to be printed on its
own line. If you don't want to limit the number of items to a particular
number, use `:infinity`. Defaults to `80`.
number, use `:infinity`.
"""
@@ -97,13 +95,13 @@ defmodule Inspect.Opts do
charlists: :infer | :as_lists | :as_charlists,
custom_options: keyword,
inspect_fun: (any, t -> Inspect.Algebra.t()),
limit: non_neg_integer | :infinity,
limit: pos_integer | :infinity,
pretty: boolean,
printable_limit: non_neg_integer | :infinity,
printable_limit: pos_integer | :infinity,
safe: boolean,
structs: boolean,
syntax_colors: [{color_key, IO.ANSI.ansidata()}],
width: non_neg_integer | :infinity
width: pos_integer | :infinity
}
end
@@ -150,7 +148,7 @@ defmodule Inspect.Algebra do
iex> Inspect.Algebra.format(doc, 80)
["a", " ", "b"]
Note that the break was represented as is, because we haven't reached
Notice the break was represented as is, because we haven't reached
a line limit. Once we do, it is replaced by a newline:
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
@@ -268,9 +266,6 @@ defmodule Inspect.Algebra do
when is_binary(doc) or doc in [:doc_nil, :doc_line] or
(is_tuple(doc) and elem(doc, 0) in @docs)
defguardp is_limit(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
# Elixir + Inspect.Opts conveniences
@doc """
@@ -402,14 +397,13 @@ defmodule Inspect.Algebra do
{:lists.reverse(["..." | acc]), simple?}
end
defp container_each([term | terms], limit, opts, fun, acc, simple?)
when is_list(terms) and is_limit(limit) do
defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) do
limit = decrement(limit)
doc = fun.(term, %{opts | limit: limit})
container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc))
end
defp container_each([left | right], limit, opts, fun, acc, simple?) when is_limit(limit) do
defp container_each([left | right], limit, opts, fun, acc, simple?) do
limit = decrement(limit)
left = fun.(left, %{opts | limit: limit})
right = fun.(right, %{opts | limit: limit})
@@ -601,7 +595,7 @@ defmodule Inspect.Algebra do
iex> Inspect.Algebra.format(doc, 80)
["a", "\t", "b"]
Note that the break was represented with the given string, because we didn't
Notice the break was represented with the given string, because we didn't
reach a line limit. Once we do, it is replaced by a newline:
iex> break = Inspect.Algebra.break("\t")
@@ -890,7 +884,7 @@ defmodule Inspect.Algebra do
"""
@spec format(t, non_neg_integer | :infinity) :: iodata
def format(doc, width) when is_doc(doc) and is_width(width) do
def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
format(width, 0, [{0, :flat, doc}])
end
@@ -906,12 +900,7 @@ defmodule Inspect.Algebra do
#
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
@spec fits?(
width :: non_neg_integer(),
column :: non_neg_integer(),
break? :: boolean(),
entries
) :: boolean()
@spec fits?(width :: integer(), column :: integer(), break? :: boolean(), entries) :: boolean()
when entries:
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
@@ -972,9 +961,7 @@ defmodule Inspect.Algebra do
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
@spec format(width :: non_neg_integer() | :infinity, column :: non_neg_integer(), [
{integer, mode, t}
]) :: [binary]
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
defp format(_, _, []), 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)]
+3 -20
View File
@@ -334,28 +334,11 @@ defmodule IO do
)
end
@doc false
def warn_once(key, message, stacktrace_drop_levels) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
if :elixir_config.warn(key, stacktrace) do
warn(message, stacktrace)
else
:ok
end
end
@doc """
Writes a `message` to stderr, along with the current stacktrace.
It returns `:ok` if it succeeds.
Do not call this function at the tail of another function. Due to tail
call optimization, a stacktrace entry would not be added and the
stacktrace would be incorrectly trimmed. Therefore make sure at least
one expression (or an atom such as `:ok`) follows the `IO.warn/1` call.
## Examples
IO.warn("variable bar is unused")
@@ -447,8 +430,8 @@ defmodule IO do
See `IO.getn/3` for a description of return values.
"""
@spec getn(device | chardata | String.Chars.t(), pos_integer | chardata | String.Chars.t()) ::
chardata | nodata
@spec getn(chardata | String.Chars.t(), pos_integer) :: chardata | nodata
@spec getn(device, chardata | String.Chars.t()) :: chardata | nodata
def getn(prompt, count \\ 1)
def getn(prompt, count) when is_integer(count) and count > 0 do
@@ -599,7 +582,7 @@ defmodule IO do
The operation is Unicode unsafe.
Note that this function treats integers in the given IO data as
Notice that this function treats integers in the given IO data as
raw bytes and does not perform any kind of encoding conversion.
If you want to convert from a charlist to a UTF-8-encoded string,
use `chardata_to_string/1` instead. For more information about
-25
View File
@@ -21,31 +21,6 @@ defmodule IO.ANSI do
[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code)
are characters embedded in text used to control formatting, color, and
other output options on video text terminals.
ANSI escapes are typically enabled on all Unix terminals. They are also
available on Windows consoles from Windows 10, although it must be
explicitly enabled for the current user in the registry by running the
following command:
reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1
After running the command above, you must restart your current console.
## Examples
Because the ANSI escape sequences are embedded in text, the normal usage of
these functions is to concatenate their output with text.
formatted_text = IO.ANSI.blue_background() <> "Example" <> IO.ANSI.reset()
IO.puts(formatted_text)
A higher level and more convenient API is also available via `IO.ANSI.format/1`,
where you use atoms to represent each ANSI escape sequence and by default
checks if ANSI is enabled:
IO.puts(IO.ANSI.format([:blue_background, "Example"]))
In case ANSI is disabled, the ANSI escape sequences are simply discarded.
"""
import IO.ANSI.Sequence
+48 -258
View File
@@ -1,7 +1,6 @@
defmodule IO.ANSI.Docs do
@moduledoc false
@bullet_text "• "
@bullets [?*, ?-, ?+]
@spaces [" ", "\n", "\t"]
@@ -47,20 +46,15 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_headings([String.t()], keyword) :: :ok
def print_headings(headings, options \\ []) do
@spec print_heading(String.t(), keyword) :: :ok
def print_heading(heading, options \\ []) do
IO.puts(IO.ANSI.reset())
options = Keyword.merge(default_options(), options)
newline_after_block(options)
width = options[:width]
for heading <- headings do
padding = div(width + String.length(heading), 2)
heading = String.pad_leading(heading, padding)
heading = if options[:enabled], do: String.pad_trailing(heading, width), else: heading
write(:doc_title, heading, options)
end
newline_after_block(options)
padding = div(width + String.length(heading), 2)
heading = heading |> String.pad_leading(padding) |> String.pad_trailing(width)
write(:doc_title, heading, options)
newline_after_block()
end
@doc """
@@ -84,11 +78,11 @@ defmodule IO.ANSI.Docs do
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
IO.puts([metadata_label(key, options), ?\s, to_string(value)])
IO.puts([metadata_label(key, options), ' ', to_string(value)])
{:delegate_to, {m, f, a}}, _printed ->
label = metadata_label(:delegate_to, options)
IO.puts([label, ?\s, Exception.format_mfa(m, f, a)])
IO.puts([label, ' ', Exception.format_mfa(m, f, a)])
_metadata, printed ->
printed
@@ -96,204 +90,21 @@ defmodule IO.ANSI.Docs do
end
defp metadata_label(key, options) do
"#{color(:doc_metadata, options)}#{key}:#{maybe_reset(options)}"
if options[:enabled] do
"#{color(:doc_metadata, options)}#{key}:#{IO.ANSI.reset()}"
else
"#{key}:"
end
end
@doc """
Prints the documentation body `doc` according to `format`.
Prints the documentation body.
It takes a set of `options` defined in `default_options/0`.
In addition to the printing string, takes a set of `options`
defined in `default_options/0`.
"""
@spec print(term(), String.t(), keyword) :: :ok
def print(doc, format, options \\ [])
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
print_markdown(doc, options)
end
def print(doc, "application/erlang+html", options) when is_list(options) do
print_erlang_html(doc, options)
end
def print(_doc, format, options) when is_binary(format) and is_list(options) do
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
end
## Erlang+html
def print_erlang_html(doc, options) do
options = Keyword.merge(default_options(), options)
IO.write(traverse_erlang_html(doc, "", options))
end
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
text
end
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
for node <- nodes do
traverse_erlang_html(node, indent, options)
end
end
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
prefix = indent <> quote_prefix(options)
content =
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.trim_trailing()
[
prefix,
class |> to_string() |> String.upcase(),
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
]
|> newline_cons()
end
defp traverse_erlang_html({:p, _, entries}, indent, options) do
[indent | handle_erlang_html_text(entries, indent, options)]
end
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
end
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
end
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
end
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
end
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
end
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
end
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
[]
end
defp traverse_erlang_html({:i, _, entries}, indent, options) do
inline_text("_", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:em, _, entries}, indent, options) do
inline_text("*", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:code, _, entries}, indent, options) do
inline_text("`", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
string =
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
end
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
if href = attributes[:href] do
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
else
traverse_erlang_html(entries, indent, options)
end
end
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
traverse_erlang_html(entries, indent, options)
end
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
["#{indent} ", @bullet_text | handle_erlang_html_text(entries, indent <> " ", options)]
end
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
end
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
if attributes[:class] == "types" do
types =
for {:li, _, lines} <- entries,
line <- lines,
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
if types != [] do
["#{indent}Typespecs:\n\n", types, ?\n]
else
[]
end
else
for {:li, _, lines} <- entries do
["#{indent} ", @bullet_text | handle_erlang_html_text(lines, indent <> " ", options)]
end
end
end
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
[
"#{indent} ",
Integer.to_string(i),
". " | handle_erlang_html_text(lines, indent <> " ", options)
]
end
end
defp traverse_erlang_html({tag, _, entries}, indent, options) do
[
indent <> "<#{tag}>\n",
traverse_erlang_html(entries, indent <> " ", options)
|> IO.iodata_to_binary()
|> String.trim_trailing(),
"\n" <> indent <> "</#{tag}>"
]
|> newline_cons()
end
defp newline_cons(text) do
[text | "\n\n"]
end
defp handle_erlang_html_text(entries, indent, options) do
if Enum.all?(entries, &inline_html?/1) do
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.split(@spaces)
|> wrap_text(options[:width], indent, true, "", [])
|> tl()
|> newline_cons()
else
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.trim_leading()
end
end
defp inline_html?(binary) when is_binary(binary), do: true
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :br], do: true
defp inline_html?(_), do: false
## Markdown
def print_markdown(doc, options) do
@spec print(String.t(), keyword) :: :ok
def print(doc, options \\ []) do
options = Keyword.merge(default_options(), options)
doc
@@ -370,16 +181,16 @@ defmodule IO.ANSI.Docs do
end
end
### Headings
## Headings
defp write_heading(heading, rest, text, indent, options) do
write_text(text, indent, options)
write(:doc_headings, heading, options)
newline_after_block(options)
newline_after_block()
process(rest, [], "", options)
end
### Quotes
## Quotes
defp process_quote([], lines, indent, options) do
write_quote(lines, indent, options, false)
@@ -412,19 +223,21 @@ defmodule IO.ANSI.Docs do
)
end
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{IO.ANSI.reset()}"
defp write_empty_quote_line(options) do
options
|> quote_prefix()
|> IO.puts()
end
### Lists
## Lists
defp process_rest(stripped, rest, count, text, indent, options) do
case stripped do
<<bullet, ?\s, item::binary>> when bullet in @bullets ->
write_text(text, indent, options)
process_list(@bullet_text, item, rest, count, indent, options)
process_list("• ", item, rest, count, indent, options)
<<d1, ?., ?\s, item::binary>> when d1 in ?0..?9 ->
write_text(text, indent, options)
@@ -444,12 +257,10 @@ defmodule IO.ANSI.Docs do
entry = if indent == "", do: " " <> entry, else: entry
new_indent = indent <> String.duplicate(" ", String.length(entry))
{contents, rest, done} =
process_list_next(rest, count, byte_size(new_indent) - byte_size(indent), [])
{contents, rest, done} = process_list_next(rest, count, byte_size(new_indent), [])
process(contents, [indent <> entry <> line, :no_wrap], new_indent, options)
if done, do: newline_after_block(options)
if done, do: newline_after_block()
process(rest, [], indent, options)
end
@@ -490,7 +301,7 @@ defmodule IO.ANSI.Docs do
end
end
### Text
## Text
defp write_text(text, indent, options) do
case Enum.reverse(text) do
@@ -514,7 +325,7 @@ defmodule IO.ANSI.Docs do
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
unless no_wrap, do: newline_after_block(options)
unless no_wrap, do: newline_after_block()
end
defp format_text(text, options) do
@@ -523,7 +334,7 @@ defmodule IO.ANSI.Docs do
|> handle_inline(options)
end
### Code blocks
## Code blocks
defp process_code([], code, indent, options) do
write_code(code, indent, options)
@@ -562,15 +373,15 @@ defmodule IO.ANSI.Docs do
defp write_code(code, indent, options) do
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
newline_after_block(options)
newline_after_block()
end
### Tables
## Tables
defp process_table(lines, indent, options) do
{table, rest} = Enum.split_while(lines, &table_line?/1)
table_lines(table, options)
newline_after_block(options)
newline_after_block()
process(rest, [], indent, options)
end
@@ -596,7 +407,7 @@ defmodule IO.ANSI.Docs do
line
|> String.trim(" ")
|> String.trim("|")
|> String.split(~r{(?<!\\)\|})
|> String.split("|")
|> Enum.map(&render_column(&1, options))
end
@@ -713,7 +524,7 @@ defmodule IO.ANSI.Docs do
defp strip_spaces(rest, acc, _max), do: {rest, acc}
defp write(style, string, options) do
IO.puts([color(style, options), string, maybe_reset(options)])
IO.puts([color(style, options), string, IO.ANSI.reset()])
end
defp write_with_wrap([], _available, _indent, _first, _prefix) do
@@ -723,7 +534,7 @@ defmodule IO.ANSI.Docs do
defp write_with_wrap(words, available, indent, first, prefix) do
words
|> wrap_text(available, indent, first, prefix, [])
|> tl()
|> Enum.join("\n")
|> IO.puts()
end
@@ -736,7 +547,7 @@ defmodule IO.ANSI.Docs do
{words, rest} = take_words(words, available - prefix_length, [])
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
wrap_text(rest, available, indent, false, prefix, [line, ?\n | wrapped_lines])
wrap_text(rest, available, indent, false, prefix, [line | wrapped_lines])
end
defp take_words([word | words], available, acc) do
@@ -810,7 +621,7 @@ defmodule IO.ANSI.Docs do
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
### Inline start
# Inline start
defp handle_inline(<<?*, ?*, rest::binary>>, options) do
handle_inline(rest, ?d, ["**"], [], options)
@@ -824,7 +635,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, nil, [], [], options)
end
### Inline delimiters
# Inline delimiters
defp handle_inline(<<delimiter, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
when rest != "" and delimiter in @delimiters do
@@ -841,7 +652,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, ?`, ["`"], [Enum.reverse(buffer) | acc], options)
end
### Clauses for handling escape
# Clauses for handling escape
defp handle_inline(<<?\\, ?\\, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
when rest != "" do
@@ -862,7 +673,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, limit, [mark | buffer], acc, options)
end
### Inline end
# Inline end
defp handle_inline(<<?*, ?*, delimiter, rest::binary>>, ?d, buffer, acc, options)
when delimiter in @delimiters do
@@ -890,7 +701,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, nil, [], [inline_buffer(buffer, options) | acc], options)
end
### Catch all
# Catch all
defp handle_inline(<<char, rest::binary>>, mark, buffer, acc, options) do
handle_inline(rest, mark, [char | buffer], acc, options)
@@ -901,24 +712,8 @@ defmodule IO.ANSI.Docs do
end
defp inline_buffer(buffer, options) do
[mark | t] = Enum.reverse(buffer)
inline_text(mark, t, options)
end
## Helpers
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
defp heading(text, n, options) do
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
end
defp inline_text(mark, text, options) do
if options[:enabled] do
[[color_for(mark, options) | text] | IO.ANSI.reset()]
else
[[mark | text] | mark]
end
[h | t] = Enum.reverse([IO.ANSI.reset() | buffer])
[color_for(h, options) | t]
end
defp color_for(mark, colors) do
@@ -931,14 +726,9 @@ defmodule IO.ANSI.Docs do
end
defp color(style, colors) do
IO.ANSI.format_fragment(colors[style], colors[:enabled])
color = colors[style]
IO.ANSI.format_fragment(color, colors[:enabled])
end
defp newline_after_block(options) do
IO.puts(maybe_reset(options))
end
defp maybe_reset(options) do
if options[:enabled], do: IO.ANSI.reset(), else: ""
end
defp newline_after_block, do: IO.puts(IO.ANSI.reset())
end
+88 -279
View File
@@ -93,7 +93,7 @@ defmodule Kernel do
### Protocols
Protocols add polymorphic dispatch to Elixir. They are contracts
implementable by data types. See `Protocol` for more information on
implementable by data types. See `defprotocol/2` for more information on
protocols. Elixir provides the following protocols in the standard library:
* `Collectable` - collects data into a data type
@@ -127,19 +127,19 @@ defmodule Kernel do
Elixir documentation also includes supporting documents under the
"Pages" section. Those are:
* [Compatibility and Deprecations](compatibility-and-deprecations.md) - lists
* [Compatibility and Deprecations](compatibility-and-deprecations.html) - lists
compatibility between every Elixir version and Erlang/OTP, release schema;
lists all deprecated functions, when they were deprecated and alternatives
* [Library Guidelines](library-guidelines.md) - general guidelines, anti-patterns,
* [Library Guidelines](library-guidelines.html) - general guidelines, anti-patterns,
and rules for those writing libraries
* [Naming Conventions](naming-conventions.md) - naming conventions for Elixir code
* [Operators](operators.md) - lists all Elixir operators and their precedences
* [Patterns and Guards](patterns-and-guards.md) - an introduction to patterns,
* [Naming Conventions](naming-conventions.html) - naming conventions for Elixir code
* [Operators](operators.html) - lists all Elixir operators and their precedence
* [Patterns and Guards](patterns-and-guards.html) - an introduction to patterns,
guards, and extensions
* [Syntax Reference](syntax-reference.md) - the language syntax reference
* [Typespecs](typespecs.md)- types and function specifications, including list of types
* [Unicode Syntax](unicode-syntax.md) - outlines Elixir support for Unicode
* [Writing Documentation](writing-documentation.md) - guidelines for writing
* [Syntax Reference](syntax-reference.html) - the language syntax reference
* [Typespecs](typespecs.html)- types and function specifications, including list of types
* [Unicode Syntax](unicode-syntax.html) - outlines Elixir support for Unicode
* [Writing Documentation](writing-documentation.html) - guidelines for writing
documentation in Elixir
## Guards
@@ -156,7 +156,7 @@ defmodule Kernel do
or equal to 16. Guards also support joining multiple conditions with
`and` and `or`. The whole guard is true if all guard expressions will
evaluate to `true`. A more complete introduction to guards is available
[in the "Patterns and Guards" page](patterns-and-guards.md).
[in the "Patterns and Guards" page](patterns-and-guards.html).
## Inlining
@@ -1193,7 +1193,7 @@ defmodule Kernel do
end
@doc """
Arithmetic addition operator.
Arithmetic addition.
Allowed in guard tests. Inlined by the compiler.
@@ -1213,7 +1213,7 @@ defmodule Kernel do
end
@doc """
Arithmetic subtraction operator.
Arithmetic subtraction.
Allowed in guard tests. Inlined by the compiler.
@@ -1233,7 +1233,7 @@ defmodule Kernel do
end
@doc """
Arithmetic positive unary operator.
Arithmetic unary plus.
Allowed in guard tests. Inlined by the compiler.
@@ -1251,7 +1251,7 @@ defmodule Kernel do
end
@doc """
Arithmetic negative unary operator.
Arithmetic unary minus.
Allowed in guard tests. Inlined by the compiler.
@@ -1271,7 +1271,7 @@ defmodule Kernel do
end
@doc """
Arithmetic multiplication operator.
Arithmetic multiplication.
Allowed in guard tests. Inlined by the compiler.
@@ -1291,7 +1291,7 @@ defmodule Kernel do
end
@doc """
Arithmetic division operator.
Arithmetic division.
The result is always a float. Use `div/2` and `rem/2` if you want
an integer division or the remainder.
@@ -1322,7 +1322,7 @@ defmodule Kernel do
end
@doc """
List concatenation operator. Concatenates a proper list and a term, returning a list.
Concatenates a proper list and a term, returning a list.
The complexity of `a ++ b` is proportional to `length(a)`, so avoid repeatedly
appending to lists of arbitrary length, for example, `list ++ [element]`.
@@ -1360,19 +1360,13 @@ defmodule Kernel do
end
@doc """
List subtraction operator. Removes the first occurrence of an element on the left list
Removes the first occurrence of an element on the left list
for each element on the right.
Before Erlang/OTP 22, the complexity of `a -- b` was proportional to
`length(a) * length(b)`, meaning that it would be very slow if
both `a` and `b` were long lists. In such cases, consider
converting each list to a `MapSet` and using `MapSet.difference/2`.
As of Erlang/OTP 22, this operation is significantly faster even if both
lists are very long, and using `--/2` is usually faster and uses less
memory than using the `MapSet`-based alternative mentioned above.
See also the [Erlang efficiency
guide](https://erlang.org/doc/efficiency_guide/retired_myths.html).
The complexity of `a -- b` is proportional to `length(a) * length(b)`,
meaning that it will be very slow if both `a` and `b` are long lists.
In such cases, consider converting each list to a `MapSet` and using
`MapSet.difference/2`.
Inlined by the compiler.
@@ -1384,16 +1378,6 @@ defmodule Kernel do
iex> [1, 2, 3, 2, 1] -- [1, 2, 2]
[3, 1]
The `--/2` operator is right associative, meaning:
iex> [1, 2, 3] -- [2] -- [3]
[1, 3]
As it is equivalent to:
iex> [1, 2, 3] -- ([2] -- [3])
[1, 3]
"""
@spec list -- list :: list
def left -- right do
@@ -1401,9 +1385,9 @@ defmodule Kernel do
end
@doc """
Strictly boolean "not" operator.
Boolean not.
`value` must be a boolean; if it's not, an `ArgumentError` exception is raised.
`arg` must be a boolean; if it's not, an `ArgumentError` exception is raised.
Allowed in guard tests. Inlined by the compiler.
@@ -1421,9 +1405,7 @@ defmodule Kernel do
end
@doc """
Less-than operator.
Returns `true` if `left` is less than `right`.
Returns `true` if left is less than right.
All terms in Elixir can be compared with each other.
@@ -1442,9 +1424,7 @@ defmodule Kernel do
end
@doc """
Greater-than operator.
Returns `true` if `left` is more than `right`.
Returns `true` if left is more than right.
All terms in Elixir can be compared with each other.
@@ -1463,9 +1443,7 @@ defmodule Kernel do
end
@doc """
Less-than or equal to operator.
Returns `true` if `left` is less than or equal to `right`.
Returns `true` if left is less than or equal to right.
All terms in Elixir can be compared with each other.
@@ -1484,9 +1462,7 @@ defmodule Kernel do
end
@doc """
Greater-than or equal to operator.
Returns `true` if `left` is more than or equal to `right`.
Returns `true` if left is more than or equal to right.
All terms in Elixir can be compared with each other.
@@ -1505,7 +1481,7 @@ defmodule Kernel do
end
@doc """
Equal to operator. Returns `true` if the two terms are equal.
Returns `true` if the two terms are equal.
This operator considers 1 and 1.0 to be equal. For stricter
semantics, use `===/2` instead.
@@ -1530,8 +1506,6 @@ defmodule Kernel do
end
@doc """
Not equal to operator.
Returns `true` if the two terms are not equal.
This operator considers 1 and 1.0 to be equal. For match
@@ -1557,8 +1531,6 @@ defmodule Kernel do
end
@doc """
Strictly equal to operator.
Returns `true` if the two terms are exactly equal.
The terms are only considered to be exactly equal if they
@@ -1586,10 +1558,7 @@ defmodule Kernel do
end
@doc """
Strictly not equal to operator.
Returns `true` if the two terms are not exactly equal.
See `===/2` for a definition of what is considered "exactly equal".
All terms in Elixir can be compared with each other.
@@ -1660,7 +1629,7 @@ defmodule Kernel do
end
@doc """
Strictly boolean "or" operator.
Boolean or.
If `left` is `true`, returns `true`; otherwise returns `right`.
@@ -1674,13 +1643,9 @@ defmodule Kernel do
iex> true or false
true
iex> false or 42
42
iex> 42 or false
** (BadBooleanError) expected a boolean on left-side of "or", got: 42
"""
@doc guard: true
defmacro left or right do
@@ -1692,7 +1657,7 @@ defmodule Kernel do
end
@doc """
Strictly boolean "and" operator.
Boolean and.
If `left` is `false`, returns `false`; otherwise returns `right`.
@@ -1705,13 +1670,9 @@ defmodule Kernel do
iex> true and false
false
iex> true and "yay!"
"yay!"
iex> "yay!" and true
** (BadBooleanError) expected a boolean on left-side of "and", got: "yay!"
"""
@doc guard: true
defmacro left and right do
@@ -1735,9 +1696,9 @@ defmodule Kernel do
end
@doc """
Boolean "not" operator.
Boolean not.
Receives any value (not just booleans) and returns `true` if `value`
Receives any argument (not just booleans) and returns `true` if the argument
is `false` or `nil`; returns `false` otherwise.
Not allowed in guard clauses.
@@ -1780,7 +1741,7 @@ defmodule Kernel do
end
@doc """
Binary concatenation operator. Concatenates two binaries.
Concatenates two binaries.
## Examples
@@ -1950,7 +1911,7 @@ defmodule Kernel do
Works like `raise/1` but does not generate a new stacktrace.
Note that `__STACKTRACE__` can be used inside catch/rescue
Notice that `__STACKTRACE__` can be used inside catch/rescue
to retrieve the current stacktrace.
## Examples
@@ -2022,12 +1983,11 @@ defmodule Kernel do
end
@doc """
Text-based match operator. Matches the term on the `left`
against the regular expression or string on the `right`.
Matches the term on the `left` against the regular expression or string on the
`right`.
If `right` is a regular expression, returns `true` if `left` matches right.
If `right` is a string, returns `true` if `left` contains `right`.
Returns `true` if `left` matches `right` (if it's a regular expression)
or contains `right` (if it's a string).
## Examples
@@ -2037,18 +1997,12 @@ defmodule Kernel do
iex> "abcd" =~ ~r/e/
false
iex> "abcd" =~ ~r//
true
iex> "abcd" =~ "bc"
true
iex> "abcd" =~ "ad"
false
iex> "abcd" =~ "abcd"
true
iex> "abcd" =~ ""
true
@@ -2296,134 +2250,6 @@ defmodule Kernel do
end
end
@doc """
Returns true if `term` is a struct of `name`; otherwise returns `false`.
Allowed in guard tests.
## Examples
iex> is_struct(URI.parse("/"), URI)
true
iex> is_struct(URI.parse("/"), Macro.Env)
false
"""
@doc since: "1.11.0", guard: true
defmacro is_struct(term, name) do
case __CALLER__.context do
nil ->
quote do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name} -> true
_ -> false
end
_ ->
raise ArgumentError
end
end
:match ->
invalid_match!(:is_struct)
:guard ->
quote do
is_map(unquote(term)) and
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name)
end
end
end
@doc """
Returns true if `term` is an exception; otherwise returns `false`.
Allowed in guard tests.
## Examples
iex> is_exception(%RuntimeError{})
true
iex> is_exception(%{})
false
"""
@doc since: "1.11.0", guard: true
defmacro is_exception(term) do
case __CALLER__.context do
nil ->
quote do
case unquote(term) do
%_{__exception__: true} -> true
_ -> false
end
end
:match ->
invalid_match!(:is_exception)
:guard ->
quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end
end
end
@doc """
Returns true if `term` is an exception of `name`; otherwise returns `false`.
Allowed in guard tests.
## Examples
iex> is_exception(%RuntimeError{}, RuntimeError)
true
iex> is_exception(%RuntimeError{}, Macro.Env)
false
"""
@doc since: "1.11.0", guard: true
defmacro is_exception(term, name) do
case __CALLER__.context do
nil ->
quote do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: true} -> true
_ -> false
end
_ ->
raise ArgumentError
end
end
:match ->
invalid_match!(:is_exception)
:guard ->
quote do
is_map(unquote(term)) and
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end
end
end
@doc """
Gets a value from a nested structure.
@@ -2511,12 +2337,6 @@ defmodule Kernel do
function. If the key is a function, it will be invoked
as specified in `get_and_update_in/3`.
`data` is a nested structure (that is, a map, keyword
list, or struct that implements the `Access` behaviour).
The `fun` argument receives the value of `key` (or `nil`
if `key` is not present) and the result replaces the value
in the structure.
## Examples
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
@@ -3062,7 +2882,7 @@ defmodule Kernel do
end
@doc """
Module attribute unary operator. Reads and writes attributes in the current module.
Reads and writes attributes of the current module.
The canonical example for attributes is annotating that a module
implements an OTP behaviour, such as `GenServer`:
@@ -3086,7 +2906,7 @@ defmodule Kernel do
will be available at compile-time. Custom attributes may be configured to
behave closer to Erlang by using `Module.register_attribute/3`.
Finally, note that attributes can also be read inside functions:
Finally, notice that attributes can also be read inside functions:
defmodule MyServer do
@my_data 11
@@ -3423,7 +3243,7 @@ defmodule Kernel do
end
@doc """
Range creation operator. Returns a range with the specified `first` and `last` integers.
Returns a range with the specified `first` and `last` integers.
If last is larger than first, the range will be increasing from
first to last. If first is larger than last, the range will be
@@ -3478,8 +3298,6 @@ defmodule Kernel do
end
@doc """
Boolean "and" operator.
Provides a short-circuit operator that evaluates and returns
the second expression only if the first one evaluates to a truthy value
(neither `false` nor `nil`). Returns the first expression
@@ -3519,8 +3337,6 @@ defmodule Kernel do
end
@doc """
Boolean "or" operator.
Provides a short-circuit operator that evaluates and returns the second
expression only if the first one does not evaluate to a truthy value (that is,
it is either `nil` or `false`). Returns the first expression otherwise.
@@ -3700,7 +3516,7 @@ defmodule Kernel do
end
@doc """
Membership operator. Checks if the element on the left-hand side is a member of the
Checks if the element on the left-hand side is a member of the
collection on the right-hand side.
## Examples
@@ -3756,7 +3572,7 @@ defmodule Kernel do
"""
@doc guard: true
defmacro left in right do
in_body? = __CALLER__.context == nil
in_module? = __CALLER__.context == nil
expand =
case bootstrapped?(Macro) do
@@ -3765,7 +3581,7 @@ defmodule Kernel do
end
case expand.(right) do
[] when not in_body? ->
[] when not in_module? ->
false
[] ->
@@ -3774,28 +3590,28 @@ defmodule Kernel do
false
end
[head | tail] = list when not in_body? ->
in_list(left, head, tail, expand, list, in_body?)
[head | tail] = list when not in_module? ->
in_var(in_module?, left, &in_list(&1, head, tail, expand, list, in_module?))
[_ | _] = list when in_body? ->
[_ | _] = list when in_module? ->
case ensure_evaled(list, {0, []}, expand) do
{[head | tail], {_, []}} ->
in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
in_var(in_module?, left, &in_list(&1, head, tail, expand, list, in_module?))
{[head | tail], {_, vars_values}} ->
{vars, values} = :lists.unzip(:lists.reverse(vars_values))
is_in_list = &in_list(&1, head, tail, expand, list, in_body?)
is_in_list = &in_list(&1, head, tail, expand, list, in_module?)
quote do
{unquote_splicing(vars)} = {unquote_splicing(values)}
unquote(in_var(in_body?, left, is_in_list))
unquote(in_var(in_module?, left, is_in_list))
end
end
{:%{}, _meta, [__struct__: Elixir.Range, first: first, last: last]} ->
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last)))
in_var(in_module?, left, &in_range(&1, expand.(first), expand.(last)))
right when in_body? ->
right when in_module? ->
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
%{__struct__: Elixir.Range, first: _, last: _} ->
@@ -3904,12 +3720,18 @@ defmodule Kernel do
end
end
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
:lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
defp in_list(left, head, tail, expand, right, in_module?) do
[head | tail] =
:lists.foldl(
&[comp(left, &1, expand, right, in_module?) | &2],
[],
[head | tail]
)
:lists.foldl(&quote(do: :erlang.orelse(unquote(&1), unquote(&2))), head, tail)
end
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
defp comp(left, {:|, _, [head, tail]}, expand, right, in_module?) do
case expand.(tail) do
[] ->
quote(do: :erlang."=:="(unquote(left), unquote(head)))
@@ -3918,11 +3740,11 @@ defmodule Kernel do
quote do
:erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
unquote(in_list(left, tail_head, tail, expand, right, in_module?))
)
end
tail when in_body? ->
tail when in_module? ->
quote do
:erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)),
@@ -3935,7 +3757,7 @@ defmodule Kernel do
end
end
defp comp(left, right, _expand, _right, _in_body?) do
defp comp(left, right, _expand, _right, _in_module?) do
quote(do: :erlang."=:="(unquote(left), unquote(right)))
end
@@ -3958,20 +3780,13 @@ defmodule Kernel do
end
@doc """
Marks that the given variable should not be hygienized.
When used inside quoting, marks that the given variable should
not be hygienized.
This macro expects a variable and it is typically invoked
inside `Kernel.SpecialForms.quote/2` to mark that a variable
should not be hygienized. See `Kernel.SpecialForms.quote/2`
for more information.
## Examples
iex> Kernel.var!(example) = 1
1
iex> Kernel.var!(example)
1
The argument can be either a variable unquoted or in standard tuple form
`{name, meta, context}`.
Check `Kernel.SpecialForms.quote/2` for more information.
"""
defmacro var!(var, context \\ nil)
@@ -4272,20 +4087,20 @@ defmodule Kernel do
## `rescue`/`catch`/`after`/`else`
Function bodies support `rescue`, `catch`, `after`, and `else` as `Kernel.SpecialForms.try/1`
does (known as "implicit try"). For example, the following two functions are equivalent:
does. For example, the following two functions are equivalent:
def convert(number) do
def format(value) do
try do
String.to_integer(number)
rescue
e in ArgumentError -> {:error, e.message}
format!(value)
catch
:exit, reason -> {:error, reason}
end
end
def convert(number) do
String.to_integer(number)
rescue
e in ArgumentError -> {:error, e.message}
def format(value) do
format!(value)
catch
:exit, reason -> {:error, reason}
end
"""
@@ -4908,8 +4723,8 @@ defmodule Kernel do
## Examples
For example, to write test cases using the `ExUnit` framework provided
with Elixir, a developer should `use` the `ExUnit.Case` module:
For example, in order to write test cases using the `ExUnit` framework
provided with Elixir, a developer should `use` the `ExUnit.Case` module:
defmodule AssertionTest do
use ExUnit.Case, async: true
@@ -4919,11 +4734,8 @@ defmodule Kernel do
end
end
In this example, Elixir will call the `__using__/1` macro in the
`ExUnit.Case` module with the keyword list `[async: true]` as its
argument.
In other words, `use/2` translates to:
In this example, `ExUnit.Case.__using__/1` is called with the keyword list
`[async: true]` as its argument; `use/2` translates to:
defmodule AssertionTest do
require ExUnit.Case
@@ -4934,7 +4746,7 @@ defmodule Kernel do
end
end
where `ExUnit.Case` defines the `__using__/1` macro:
`ExUnit.Case` will then define the `__using__/1` macro:
defmodule ExUnit.Case do
defmacro __using__(opts) do
@@ -5680,12 +5492,9 @@ defmodule Kernel do
end
@doc false
# TODO: Remove on v2.0 (also hard-coded in elixir_dispatch)
@deprecated "Use Kernel.to_charlist/1 instead"
defmacro to_char_list(arg) do
IO.warn(
"Kernel.to_char_list/1 is deprecated, use Kernel.to_charlist/1 instead",
Macro.Env.stacktrace(__CALLER__)
)
quote(do: Kernel.to_charlist(unquote(arg)))
end
end
+2 -6
View File
@@ -498,12 +498,9 @@ defmodule Kernel.CLI do
verbose_opts =
if config.verbose_compile do
[each_file: &IO.puts("Compiling #{Path.relative_to_cwd(&1)}")]
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 15s)")]
else
[
each_long_compilation:
&IO.puts("Compiling #{Path.relative_to_cwd(&1)} (it's taking more than 10s)")
]
[]
end
profile_opts =
@@ -528,7 +525,6 @@ defmodule Kernel.CLI do
defp filter_patterns(pattern) do
pattern
|> Path.expand()
|> Path.wildcard()
|> :lists.usort()
|> Enum.filter(&File.regular?/1)
+21 -19
View File
@@ -29,11 +29,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :stop)
end
@doc false
def add_require(pid, module) when is_atom(module) do
:gen_server.cast(pid, {:add_require, module})
end
@doc false
def add_import(pid, module, fas, line, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_import, module, fas, line, warn})
@@ -50,8 +45,13 @@ defmodule Kernel.LexicalTracker do
end
@doc false
def import_dispatch(pid, module, fa, mode) when is_atom(module) do
:gen_server.cast(pid, {:import_dispatch, module, fa, mode})
def remote_struct(pid, module) when is_atom(module) do
:gen_server.cast(pid, {:remote_struct, module})
end
@doc false
def import_dispatch(pid, module, fa) when is_atom(module) do
:gen_server.cast(pid, {:import_dispatch, module, fa})
end
@doc false
@@ -106,7 +106,7 @@ defmodule Kernel.LexicalTracker do
state = %{
directives: %{},
references: %{},
exports: %{},
structs: %{},
cache: %{},
compile_env: :ordsets.new(),
file: nil
@@ -127,7 +127,7 @@ defmodule Kernel.LexicalTracker do
def handle_call(:references, _from, state) do
{compile, runtime} = partition(Map.to_list(state.references), [], [])
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
{:reply, {compile, Map.keys(state.structs), runtime, state.compile_env}, state}
end
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
@@ -142,13 +142,18 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | cache: Map.put(cache, key, value)}}
end
def handle_cast({:remote_struct, module}, state) do
structs = Map.put(state.structs, module, true)
{:noreply, %{state | structs: structs}}
end
def handle_cast({:remote_dispatch, module, mode}, state) do
references = add_reference(state.references, module, mode)
{:noreply, %{state | references: references}}
end
def handle_cast({:import_dispatch, module, {function, arity}, mode}, state) do
state = add_import_dispatch(state, module, function, arity, mode)
def handle_cast({:import_dispatch, module, {function, arity}}, state) do
state = add_import_dispatch(state, module, function, arity)
{:noreply, state}
end
@@ -168,10 +173,6 @@ defmodule Kernel.LexicalTracker do
{:noreply, update_in(state.compile_env, &:ordsets.add_element({app, path, return}, &1))}
end
def handle_cast({:add_require, module}, state) do
{:noreply, put_in(state.exports[module], true)}
end
def handle_cast({:add_import, module, fas, line, warn}, state) do
directives =
state.directives
@@ -226,13 +227,14 @@ defmodule Kernel.LexicalTracker do
end
end
defp add_import_dispatch(state, module, function, arity, mode) do
defp add_import_dispatch(state, module, function, arity) do
directives =
state.directives
|> add_dispatch(module, :import)
add_dispatch(state.directives, module, :import)
|> add_dispatch({module, function, arity}, :import)
references = add_reference(state.references, module, mode)
# Always compile time because we depend
# on the module at compile time
references = add_reference(state.references, module, :compile)
%{state | directives: directives, references: references}
end
+97 -153
View File
@@ -15,7 +15,7 @@ defmodule Kernel.ParallelCompiler do
always awaited on by calling `Task.await/1`
"""
@doc since: "1.6.0"
def async(fun) when is_function(fun, 0) do
def async(fun) when is_function(fun) do
if parent = :erlang.get(:elixir_compiler_pid) do
file = :erlang.get(:elixir_compiler_file)
dest = :erlang.get(:elixir_compiler_dest)
@@ -63,16 +63,12 @@ defmodule Kernel.ParallelCompiler do
* `:each_cycle` - after the given files are compiled, invokes this function
that should return the following values:
* `{:compile, modules, warnings}` - to continue compilation with a list of
further modules to compile
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
of modules because dependent modules have changed
* `{:compile, modules}` - to continue compilation with a list of further modules to compile
* `{:runtime, modules}` - to stop compilation and verify the list of modules because
dependent modules have changed
* `:long_compilation_threshold` - the timeout (in seconds) to check for modules
taking too long to compile. For each file that exceeds the threshold, the
`:each_long_compilation` callback is invoked. From Elixir v1.11, only the time
spent compiling the actual module is taken into account by the threshold, the
time spent waiting is not considered. Defaults to `10` seconds.
* `:long_compilation_threshold` - the timeout (in seconds) after the
`:each_long_compilation` callback is invoked; defaults to `15`
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
@@ -140,45 +136,42 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(files, output, options) do
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
compiler_pid = self()
:elixir_code_server.cast({:reset_warnings, compiler_pid})
schedulers = max(:erlang.system_info(:schedulers_online), 2)
beam_timestamp = Keyword.get(options, :beam_timestamp)
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = Process.send_after(self(), :threshold_check, threshold)
{outcome, state} =
outcome =
spawn_workers(files, 0, [], [], %{}, [], %{
dest: Keyword.get(options, :dest),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, []} end),
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
profile: profile_init(Keyword.get(options, :profile)),
long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
profile: Keyword.get(options, :profile),
cycle_start: System.monotonic_time(),
module_counter: 0,
output: output,
timer_ref: timer_ref,
long_compilation_threshold: threshold,
schedulers: schedulers
})
Process.cancel_timer(state.timer_ref)
# In case --warning-as-errors is enabled and there was a warning,
# compilation status will be set to error.
compilation_status = :elixir_code_server.call({:compilation_status, compiler_pid})
receive do
:threshold_check -> :ok
after
0 -> :ok
end
case {outcome, Code.get_compiler_option(:warnings_as_errors)} do
{{:ok, _, [_ | _] = warnings}, true} ->
case {outcome, compilation_status} do
{{:ok, _, warnings}, :error} ->
message = "Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
{:error, warnings, []}
{{:error, errors, warnings}, :error} ->
{:error, errors ++ warnings, []}
{{:ok, outcome, warnings}, _} ->
{:ok, write_module_binaries(outcome, output, beam_timestamp), warnings}
{{:error, errors, warnings}, true} ->
{:error, errors ++ warnings, []}
{{:error, errors, warnings}, _} ->
{:error, errors, warnings}
end
@@ -219,7 +212,7 @@ defmodule Kernel.ParallelCompiler do
defp verify_modules(result, warnings, dependent_modules, state) do
checker_warnings = maybe_check_modules(result, dependent_modules, state)
warnings = Enum.reverse(warnings, checker_warnings)
{{:ok, result, warnings}, state}
{:ok, result, warnings}
end
defp maybe_check_modules(result, runtime_modules, state) do
@@ -251,10 +244,7 @@ defmodule Kernel.ParallelCompiler do
end
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
defp profile_init(nil), do: :none
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
num_modules = length(compiled_modules) + length(runtime_modules)
@@ -262,7 +252,7 @@ defmodule Kernel.ParallelCompiler do
result
end
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
fun.()
end
@@ -284,23 +274,23 @@ defmodule Kernel.ParallelCompiler do
# Release waiting processes
defp spawn_workers([{ref, found} | t], spawned, waiting, files, result, warnings, state) do
{files, waiting} =
waiting =
case List.keytake(waiting, ref, 2) do
{{_kind, pid, ^ref, _on, _defining, _deadlock}, waiting} ->
send(pid, {ref, found})
{update_timing(files, pid, :waiting), waiting}
waiting
nil ->
# In case the waiting process died (for example, it was an async process),
# it will no longer be on the list. So we need to take it into account here.
{files, waiting}
waiting
end
spawn_workers(t, spawned, waiting, files, result, warnings, state)
end
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, state) do
%{output: output, dest: dest} = state
%{output: output, long_compilation_threshold: threshold, dest: dest} = state
parent = self()
file = Path.expand(file)
@@ -323,34 +313,24 @@ defmodule Kernel.ParallelCompiler do
exit(:shutdown)
end)
file_data = %{
pid: pid,
ref: ref,
file: file,
timestamp: System.monotonic_time(),
compiling: 0,
waiting: 0,
warned: false
}
files = [file_data | files]
timer_ref = Process.send_after(self(), {:timed_out, pid}, threshold * 1000)
files = [{pid, ref, file, timer_ref} | files]
spawn_workers(queue, spawned + 1, waiting, files, result, warnings, state)
end
# No more queue, nothing waiting, this cycle is done
defp spawn_workers([], 0, [], [], result, warnings, state) do
cycle_return = each_cycle_return(state.each_cycle.())
state = cycle_timing(result, state)
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
case each_cycle_return(state.each_cycle.()) do
{:runtime, dependent_modules} ->
verify_modules(result, warnings, dependent_modules, state)
{:compile, [], extra_warnings} ->
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, []} ->
verify_modules(result, warnings, [], state)
{:compile, more, extra_warnings} ->
spawn_workers(more, 0, [], [], result, extra_warnings ++ warnings, state)
{:compile, more} ->
spawn_workers(more, 0, [], [], result, warnings, state)
end
end
@@ -361,7 +341,7 @@ defmodule Kernel.ParallelCompiler do
[],
1,
[{_, pid, ref, _, _, _}] = waiting,
[%{pid: pid}] = files,
[{pid, _, _, _}] = files,
result,
warnings,
state
@@ -392,7 +372,7 @@ defmodule Kernel.ParallelCompiler do
true ->
errors = handle_deadlock(waiting, files)
{{:error, errors, warnings}, state}
{:error, errors, warnings}
end
end
@@ -418,11 +398,8 @@ defmodule Kernel.ParallelCompiler do
end
end
defp cycle_timing(_result, %{profile: :none} = state) do
state
end
defp cycle_timing(result, %{profile: {:time, cycle_start, module_counter}} = state) do
defp cycle_timing(result, %{profile: :time} = state) do
%{cycle_start: cycle_start, module_counter: module_counter} = state
num_modules = count_modules(result)
diff_modules = num_modules - module_counter
now = System.monotonic_time()
@@ -433,24 +410,27 @@ defmodule Kernel.ParallelCompiler do
"[profile] Finished compilation cycle of #{diff_modules} modules in #{time}ms"
)
%{state | profile: {:time, now, num_modules}}
%{state | cycle_start: now, module_counter: num_modules}
end
defp cycle_timing(_result, %{profile: nil} = state) do
state
end
defp count_modules(result) do
Enum.count(result, &match?({{:module, _}, _}, &1))
end
# TODO: Deprecate other returns on v1.14
defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings}
defp each_cycle_return({kind, modules}), do: {kind, modules, []}
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules, []}
# TODO: Deprecate on v1.14
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules}
defp each_cycle_return(other), do: other
# The goal of this function is to find leaves in the dependency graph,
# i.e. to find code that depends on code that we know is not being defined.
# Note that not all files have been compile yet, so they may not be in waiting.
defp without_definition(waiting, files) do
nillify_empty(
for %{pid: pid} <- files,
for {pid, _, _, _} <- files,
{_, ^pid, ref, on, _, _} <- List.wrap(List.keyfind(waiting, pid, 1)),
not Enum.any?(waiting, fn {_, _, _, _, defining, _} -> on in defining end),
do: {ref, :not_found}
@@ -491,6 +471,7 @@ defmodule Kernel.ParallelCompiler do
for {:module, _, ref, ^module, _defining, _deadlock} <- waiting,
do: {ref, :found}
cancel_waiting_timer(files, child)
result = Map.put(result, {:module, module}, {binary, module_map})
spawn_workers(available ++ queue, spawned, waiting, files, result, warnings, state)
@@ -501,33 +482,24 @@ defmodule Kernel.ParallelCompiler do
{:waiting, kind, child, ref, on, defining, deadlock?} ->
# If we already got what we were waiting for, do not put it on waiting.
# If we're waiting on ourselves, send :found so that we can crash with
# a better error.
{files, waiting} =
# Alternatively, we're waiting on ourselves,
# send :found so that we can crash with a better error.
waiting =
if Map.has_key?(result, {kind, on}) or on in defining do
send(child, {ref, :found})
{files, waiting}
waiting
else
files = update_timing(files, child, :compiling)
{files, [{kind, child, ref, on, defining, deadlock?} | waiting]}
[{kind, child, ref, on, defining, deadlock?} | waiting]
end
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
:threshold_check ->
files =
for data <- files do
if data.warned or List.keymember?(waiting, data.pid, 1) do
data
else
data = update_timing(data, :compiling)
data = maybe_warn_long_compilation(data, state)
data
end
end
{:timed_out, child} ->
case List.keyfind(files, child, 0) do
{^child, _, file, _} -> state.each_long_compilation.(file)
_ -> :ok
end
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
{:warning, file, line, message} ->
@@ -539,9 +511,10 @@ defmodule Kernel.ParallelCompiler do
{:file_ok, child_pid, ref, file, lexical} ->
state.each_file.(file, lexical)
send(child_pid, ref)
cancel_waiting_timer(files, child_pid)
discard_down(child_pid)
new_files = discard_and_maybe_log_file(files, child_pid, state)
new_files = List.keydelete(files, child_pid, 0)
# Sometimes we may have spurious entries in the waiting list
# because someone invoked try/rescue UndefinedFunctionError
@@ -549,77 +522,28 @@ defmodule Kernel.ParallelCompiler do
spawn_workers(queue, spawned - 1, new_waiting, new_files, result, warnings, state)
{:file_cancel, child_pid} ->
cancel_waiting_timer(files, child_pid)
discard_down(child_pid)
new_files = Enum.reject(files, &(&1.pid == child_pid))
new_files = List.keydelete(files, child_pid, 0)
spawn_workers(queue, spawned - 1, waiting, new_files, result, warnings, state)
{:file_error, child_pid, file, {kind, reason, stack}} ->
print_error(file, kind, reason, stack)
cancel_waiting_timer(files, child_pid)
discard_down(child_pid)
files |> Enum.reject(&(&1.pid == child_pid)) |> terminate()
{{:error, [to_error(file, kind, reason, stack)], warnings}, state}
files |> List.keydelete(child_pid, 0) |> terminate()
{:error, [to_error(file, kind, reason, stack)], warnings}
{:DOWN, ref, :process, pid, reason} ->
waiting = List.keydelete(waiting, pid, 1)
case handle_down(files, ref, reason) do
:ok -> wait_for_messages(queue, spawned - 1, waiting, files, result, warnings, state)
{:error, errors} -> {{:error, errors, warnings}, state}
{:error, errors} -> {:error, errors, warnings}
end
end
end
defp update_timing(files, pid, key) do
Enum.map(files, fn data ->
if data.pid == pid do
time = System.monotonic_time()
%{data | key => data[key] + time - data.timestamp, timestamp: time}
else
data
end
end)
end
defp update_timing(data, key) do
time = System.monotonic_time()
%{data | key => data[key] + time - data.timestamp, timestamp: time}
end
defp maybe_warn_long_compilation(data, state) do
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
if not data.warned and compiling >= state.long_compilation_threshold do
state.each_long_compilation.(data.file)
%{data | warned: true}
else
data
end
end
defp discard_and_maybe_log_file(files, pid, state) do
Enum.reject(files, fn data ->
if data.pid == pid do
data = update_timing(data, :compiling)
data = maybe_warn_long_compilation(data, state)
if state.profile != :none do
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
waiting = System.convert_time_unit(data.waiting, :native, :millisecond)
extra = if waiting > 0, do: " (plus #{waiting}ms waiting)", else: ""
IO.puts(
:stderr,
"[profile] #{Path.relative_to_cwd(data.file)} compiled in #{compiling}ms" <> extra
)
end
true
else
false
end
end)
end
defp discard_down(pid) do
receive do
{:DOWN, _, :process, ^pid, _} -> :ok
@@ -631,20 +555,24 @@ defmodule Kernel.ParallelCompiler do
end
defp handle_down(files, ref, reason) do
case Enum.find(files, &(&1.ref == ref)) do
%{pid: pid, file: file} ->
case List.keyfind(files, ref, 1) do
{child_pid, ^ref, file, _timer_ref} ->
print_error(file, :exit, reason, [])
files |> Enum.reject(&(&1.pid == pid)) |> terminate()
files
|> List.keydelete(child_pid, 0)
|> terminate()
{:error, [to_error(file, :exit, reason, [])]}
nil ->
_ ->
:ok
end
end
defp handle_deadlock(waiting, files) do
deadlock =
for %{pid: pid, file: file} <- files do
for {pid, _, file, _} <- files do
{:current_stacktrace, stacktrace} = Process.info(pid, :current_stacktrace)
Process.exit(pid, :kill)
@@ -679,8 +607,8 @@ defmodule Kernel.ParallelCompiler do
end
defp terminate(files) do
for %{pid: pid} <- files, do: Process.exit(pid, :kill)
for %{pid: pid} <- files, do: discard_down(pid)
for {pid, _, _, _} <- files, do: Process.exit(pid, :kill)
for {pid, _, _, _} <- files, do: discard_down(pid)
:ok
end
@@ -691,6 +619,22 @@ defmodule Kernel.ParallelCompiler do
])
end
defp cancel_waiting_timer(files, child_pid) do
case List.keyfind(files, child_pid, 0) do
{^child_pid, _ref, _file, timer_ref} ->
Process.cancel_timer(timer_ref)
# Let's flush the message in case it arrived before we canceled the timeout.
receive do
{:timed_out, ^child_pid} -> :ok
after
0 -> :ok
end
nil ->
:ok
end
end
defp to_error(file, kind, reason, stack) do
line = get_line(file, reason, stack)
file = Path.absname(file)
+20 -23
View File
@@ -187,7 +187,13 @@ defmodule Kernel.SpecialForms do
iex> <<0, "foo">>
<<0, 102, 111, 111>>
Binaries need to be explicitly tagged as `binary`:
Variables or any other type need to be explicitly tagged:
iex> rest = "oo"
iex> <<102, rest>>
** (ArgumentError) argument error
We can solve this by explicitly tagging it as `binary`:
iex> rest = "oo"
iex> <<102, rest::binary>>
@@ -201,12 +207,6 @@ defmodule Kernel.SpecialForms do
iex> <<"foo"::utf32>>
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
Otherwise we get an `ArgumentError` when construcing the binary:
rest = "oo"
<<102, rest>>
** (ArgumentError) argument error
## Options
Many options can be given by using `-` as separator. Order is
@@ -371,7 +371,7 @@ defmodule Kernel.SpecialForms do
defmacro unquote(:<<>>)(args), do: error!([args])
@doc """
Dot operator. Defines a remote call, a call to an anonymous function, or an alias.
Defines a remote call, a call to an anonymous function, or an alias.
The dot (`.`) in Elixir can be used for remote calls:
@@ -438,7 +438,7 @@ defmodule Kernel.SpecialForms do
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Note that we have an inner tuple, containing the atom `:.` representing
Notice we have an inner tuple, containing the atom `:.` representing
the dot as first element:
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
@@ -513,7 +513,7 @@ defmodule Kernel.SpecialForms do
Keyword.values #=> uses MyKeyword.values
Elixir.Keyword.values #=> uses Keyword.values
Note that calling `alias` without the `:as` option automatically
Notice that calling `alias` without the `:as` option automatically
sets an alias based on the last part of the module. For example:
alias Foo.Bar.Baz
@@ -599,7 +599,7 @@ defmodule Kernel.SpecialForms do
## Selector
By default, Elixir imports functions and macros from the given
module, except the ones starting with an underscore (which are
module, except the ones starting with underscore (which are
usually callbacks):
import List
@@ -617,7 +617,7 @@ defmodule Kernel.SpecialForms do
import List, only: [flatten: 1]
import String, except: [split: 2]
Note that calling `except` is always exclusive on a previously
Notice that calling `except` is always exclusive on a previously
declared `import/2`. If there is no previous import, then it applies
to all functions and macros in the module. For example:
@@ -637,7 +637,7 @@ defmodule Kernel.SpecialForms do
## Lexical scope
It is important to note that `import/2` is lexical. This means you
It is important to notice that `import/2` is lexical. This means you
can import specific macros inside specific functions:
defmodule Math do
@@ -721,11 +721,10 @@ defmodule Kernel.SpecialForms do
To retrieve the stacktrace of the current process, use
`Process.info(self(), :current_stacktrace)` instead.
"""
@doc since: "1.7.0"
defmacro __STACKTRACE__, do: error!([])
@doc """
Pin operator. Accesses an already bound variable in match clauses.
Accesses an already bound variable in match clauses. Also known as the pin operator.
## Examples
@@ -757,12 +756,12 @@ defmodule Kernel.SpecialForms do
defmacro ^var, do: error!([var])
@doc """
Match operator. Matches the value on the right against the pattern on the left.
Matches the value on the right against the pattern on the left.
"""
defmacro left = right, do: error!([left, right])
@doc """
Type operator. Used by types and bitstrings to specify types.
Used by types and bitstrings to specify types.
This operator is used in two distinct occasions in Elixir.
It is used in typespecs to specify the type of a variable,
@@ -806,7 +805,7 @@ defmodule Kernel.SpecialForms do
* The first element of the tuple is always an atom or
another tuple in the same representation.
* The second element of the tuple represents [metadata](`t:Macro.metadata/0`).
* The second element of the tuple represents [metadata](t:Macro.metadata/0).
* The third element of the tuple are the arguments for the
function call. The third argument may be an atom, which is
@@ -1064,7 +1063,7 @@ defmodule Kernel.SpecialForms do
Hygiene.no_interference()
#=> %{}
Note that, even though the alias `M` is not available
Notice that, even though the alias `M` is not available
in the context the macro is expanded, the code above works
because `M` still expands to `Map`.
@@ -1559,8 +1558,6 @@ defmodule Kernel.SpecialForms do
@doc """
Defines an anonymous function.
See `Function` for more information.
## Examples
iex> add = fn a, b -> a + b end
@@ -1598,7 +1595,7 @@ defmodule Kernel.SpecialForms do
defmacro unquote(:__block__)(args), do: error!([args])
@doc """
Caputure operator. Captures or creates an anonymous function.
Captures or creates an anonymous function.
## Capture
@@ -1748,7 +1745,7 @@ defmodule Kernel.SpecialForms do
## Variable handling
Note that variables bound in a clause do not leak to the outer context:
Notice that variables bound in a clause do not leak to the outer context:
case data do
{:ok, value} -> value
+7 -20
View File
@@ -22,9 +22,9 @@ defmodule Kernel.Typespec do
{:docs_v1, _, _, _, _, _, docs} ->
for {{:type, name, arity}, _, _, doc, _} <- docs do
case doc do
%{"en" => doc_string} -> {{name, arity}, doc_string}
:none -> {{name, arity}, nil}
:hidden -> {{name, arity}, false}
_ -> {{name, arity}, nil}
%{"en" => doc_string} -> {{name, arity}, doc_string}
end
end
@@ -83,7 +83,7 @@ defmodule Kernel.Typespec do
store_typespec(bag, kind, expr, pos)
case :ets.lookup(set, {:function, name, arity}) do
[{{:function, ^name, ^arity}, _, line, _, doc, doc_meta}] ->
[{{:function, ^name, ^arity}, line, _, doc, doc_meta}] ->
store_doc(set, kind, name, arity, line, :doc, doc, doc_meta)
_ ->
@@ -130,18 +130,11 @@ defmodule Kernel.Typespec do
store_typespec(bag, kind, expr, pos)
end
@reserved_signatures [required: 1, optional: 1]
def deftypespec(kind, expr, line, file, module, pos)
when kind in [:type, :typep, :opaque] do
{set, bag} = :elixir_module.data_tables(module)
case type_to_signature(expr) do
{name, arity} = signature when signature in @reserved_signatures ->
compile_error(
:elixir_locals.get_cached_env(pos),
"type #{name}/#{arity} is a reserved type and it cannot be defined"
)
{name, arity} when kind == :typep ->
{line, doc} = get_doc_info(set, :typedoc, line)
@@ -257,13 +250,7 @@ defmodule Kernel.Typespec do
end
if Map.has_key?(type_pairs, type_pair) do
{error_full_path, error_line} = type_pairs[type_pair]
error_relative_path = Path.relative_to_cwd(error_full_path)
compile_error(
env,
"type #{name}/#{arity} is already defined in #{error_relative_path}:#{error_line}"
)
compile_error(env, "type #{name}/#{arity} is already defined")
end
Map.put(type_pairs, type_pair, {file, line})
@@ -477,7 +464,7 @@ defmodule Kernel.Typespec do
_,
state
)
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
when is_atom(ctx1) and is_atom(ctx2) and is_integer(unit) and unit >= 0 do
line = line(meta)
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
end
@@ -502,7 +489,7 @@ defmodule Kernel.Typespec do
state
)
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
size >= 0 and unit in 1..256 do
is_integer(unit) and size >= 0 and unit >= 0 do
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
{{:type, line(meta), :binary, args}, state}
end
@@ -510,7 +497,7 @@ defmodule Kernel.Typespec do
defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
message =
"invalid binary specification, expected <<_::size>>, <<_::_*unit>>, " <>
"or <<_::size, _::_*unit>> with size being non-negative integers, and unit being an integer between 1 and 256"
"or <<_::size, _::_*unit>> with size and unit being non-negative integers"
compile_error(caller, message)
end
+40 -105
View File
@@ -1,11 +1,8 @@
defmodule Keyword do
@moduledoc """
A keyword list is a list that consists exclusively of two-element tuples.
The first element of these tuples is known as the *key*, and it must be an atom.
The second element, known as the *value*, can be any term.
Keywords are mostly used to work with optional values.
Keyword lists are lists of two-element tuples, where the first
element of the tuple is an atom and the second element can be any
value, used mostly to work with optional values.
## Examples
@@ -39,32 +36,18 @@ defmodule Keyword do
in the `Map` module. For example, `Keyword.get/3` will get the first
entry matching the given key, regardless if duplicated entries exist.
Similarly, `Keyword.put/3` and `Keyword.delete/2` ensure all duplicated
entries for a given key are removed when invoked. Note, however, that
keyword list operations need to traverse the whole list in order to find
entries for a given key are removed when invoked. Note however that
keyword list operations need to traverse the list in order to find
keys, so these operations are slower than their map counterparts.
A handful of functions exist to handle duplicated keys, for example,
`get_values/2` returns all values for a given key and `delete_first/2`
deletes just one of the existing entries.
Even though lists preserve the user ordering, the functions in
`Keyword` do not guarantee any ordering. For example, if you invoke
`Keyword.put(opts, new_key, new_value)`, there is no guarantee to
where `new_key` will be added (to the front, to the end, or
anywhere else).
Given ordering is not guaranteed, it is not recommended to pattern
match on keyword lists either. For example, a function such as:
def my_function([some_key: value, another_key: another_value])
will match
my_function([some_key: :foo, another_key: :bar])
but it won't match
my_function([another_key: :bar, some_key: :foo])
The functions in `Keyword` do not guarantee any property when it comes
to ordering. However, since a keyword list is simply a list, all the
operations defined in `Enum` and `List` can be applied too, especially
when ordering is required.
Most of the functions in this module work in linear time. This means
that, the time it takes to perform an operation grows at the same
@@ -72,8 +55,8 @@ defmodule Keyword do
## Call syntax
When keyword lists are passed as the last argument to a function,
the square brackets around the keyword list can be omitted. For
When keyword lists are passed as the last argument to a function, then
the square brackets around the keyword list can be omitted as well. For
example, the keyword list syntax:
String.split("1-0", "-", [trim: true, parts: 2])
@@ -259,13 +242,13 @@ defmodule Keyword do
Gets the value from `key` and updates it, all in one pass.
This `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the current value
is not present) and must return a two-element tuple: the "get" value
(the retrieved value, which can be operated on before being returned)
and the new value to be stored under `key`. The `fun` may also
return `:pop`, implying the current value shall be removed from the
keyword list and returned.
The returned value is a tuple with the current value returned by
The returned value is a tuple with the "get" value returned by
`fun` and a new keyword list with the updated value under `key`.
## Examples
@@ -287,9 +270,7 @@ defmodule Keyword do
{nil, [a: 1]}
"""
@spec get_and_update(t, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, value}
when current_value: value
@spec get_and_update(t, key, (value -> {get, value} | :pop)) :: {get, t} when get: term
def get_and_update(keywords, key, fun)
when is_list(keywords) and is_atom(key),
do: get_and_update(keywords, [], key, fun)
@@ -326,11 +307,11 @@ defmodule Keyword do
Gets the value from `key` and updates it. Raises if there is no `key`.
This `fun` argument receives the value of `key` and must return a
two-element tuple: the current value (the retrieved value, which can be
two-element tuple: the "get" value (the retrieved value, which can be
operated on before being returned) and the new value to be stored under
`key`.
The returned value is a tuple with the current value returned by `fun` and a new
The returned value is a tuple with the "get" value returned by `fun` and a new
keyword list with the updated value under `key`.
## Examples
@@ -351,9 +332,7 @@ defmodule Keyword do
{1, []}
"""
@spec get_and_update!(t, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, t}
when current_value: value
@spec get_and_update!(t, key, (value -> {get, value})) :: {get, t} when get: term
def get_and_update!(keywords, key, fun) do
get_and_update!(keywords, key, fun, [])
end
@@ -452,30 +431,13 @@ defmodule Keyword do
iex> Keyword.keys(a: 1, b: 2)
[:a, :b]
iex> Keyword.keys(a: 1, b: 2, a: 3)
[:a, :b, :a]
iex> Keyword.keys([{:a, 1}, {"b", 2}, {:c, 3}])
** (ArgumentError) expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, got: {"b", 2}
"""
@spec keys(t) :: [key]
def keys(keywords) when is_list(keywords) do
try do
:lists.map(
fn
{key, _} when is_atom(key) -> key
element -> throw(element)
end,
keywords
)
catch
element ->
raise ArgumentError,
"expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, " <>
"got: #{inspect(element)}"
end
:lists.map(fn {k, _} -> k end, keywords)
end
@doc """
@@ -650,41 +612,18 @@ defmodule Keyword do
end
end
@doc """
Puts a value under `key` only if the `key` already exists in `keywords`.
In the case a value is stored multiple times in the keyword list,
later occurrences are removed.
## Examples
iex> Keyword.replace([a: 1, b: 2, a: 4], :a, 3)
[a: 3, b: 2]
iex> Keyword.replace([a: 1], :b, 2)
[a: 1]
"""
@doc since: "1.11.0"
@spec replace(t, key, value) :: t
@doc false
@deprecated "Use Keyword.fetch/2 + Keyword.put/3 instead"
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
do_replace(keywords, key, value)
end
defp do_replace([{key, _} | keywords], key, value) do
[{key, value} | delete(keywords, key)]
end
defp do_replace([{_, _} = e | keywords], key, value) do
[e | do_replace(keywords, key, value)]
end
defp do_replace([], _key, _value) do
[]
case :lists.keyfind(key, 1, keywords) do
{^key, _} -> [{key, value} | delete(keywords, key)]
false -> keywords
end
end
@doc """
Puts a value under `key` only if the `key` already exists in `keywords`.
Alters the value stored under `key` to `value`, but only
if the entry `key` already exists in `keywords`.
If `key` is not present in `keywords`, a `KeyError` exception is raised.
@@ -713,7 +652,7 @@ defmodule Keyword do
[e | replace!(keywords, key, value, original)]
end
defp replace!([], key, _value, original) do
defp replace!([], key, _value, original) when is_atom(key) do
raise(KeyError, key: key, term: original)
end
@@ -883,7 +822,7 @@ defmodule Keyword do
** (KeyError) key :b not found in: [a: 1]
"""
@spec update!(t, key, (current_value :: value -> new_value :: value)) :: t
@spec update!(t, key, (value -> value)) :: t
def update!(keywords, key, fun)
when is_list(keywords) and is_atom(key) and is_function(fun, 1) do
update!(keywords, key, fun, keywords)
@@ -904,38 +843,34 @@ defmodule Keyword do
@doc """
Updates the `key` in `keywords` with the given function.
If the `key` does not exist, it inserts the given `default` value.
If the `key` does not exist, inserts the given `initial` value.
If there are duplicated keys, they are all removed and only the first one
is updated.
The default value will not be passed through the update function.
## Examples
iex> Keyword.update([a: 1], :a, 13, fn existing_value -> existing_value * 2 end)
iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
[a: 2]
iex> Keyword.update([a: 1, a: 2], :a, 13, fn existing_value -> existing_value * 2 end)
iex> Keyword.update([a: 1, a: 2], :a, 13, &(&1 * 2))
[a: 2]
iex> Keyword.update([a: 1], :b, 11, fn existing_value -> existing_value * 2 end)
iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
[a: 1, b: 11]
"""
@spec update(t, key, default :: value, (existing_value :: value -> updated_value :: value)) :: t
def update(keywords, key, default, fun)
@spec update(t, key, value, (value -> value)) :: t
def update(keywords, key, initial, fun)
def update([{key, value} | keywords], key, _default, fun) do
def update([{key, value} | keywords], key, _initial, fun) do
[{key, fun.(value)} | delete(keywords, key)]
end
def update([{_, _} = e | keywords], key, default, fun) do
[e | update(keywords, key, default, fun)]
def update([{_, _} = e | keywords], key, initial, fun) do
[e | update(keywords, key, initial, fun)]
end
def update([], key, default, _fun) when is_atom(key) do
[{key, default}]
def update([], key, initial, _fun) when is_atom(key) do
[{key, initial}]
end
@doc """
@@ -1039,7 +974,7 @@ defmodule Keyword do
end
@doc """
Returns the first value for `key` and removes all associated entries in the keyword list,
Returns the first value for `key` and removes all associated antries in the keyword list,
raising if `key` is not present.
This function behaves like `pop/3`, but raises in cases the `key` is not present in the
+3 -3
View File
@@ -1,6 +1,6 @@
defmodule List do
@moduledoc """
Linked lists hold zero, one, or more elements in the chosen order.
Linked lists hold zero, one, or more elements in the choosen order.
Lists in Elixir are specified between square brackets:
@@ -897,7 +897,7 @@ defmodule List do
* integers representing Unicode code points
* a list containing one of these three elements
Note that this function expects a list of integers representing
Notice that this function expects a list of integers representing
Unicode code points. If you have a list of bytes, you must instead use
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
@@ -952,7 +952,7 @@ defmodule List do
Converts a list of integers representing Unicode code points, lists or
strings into a charlist.
Note that this function expects a list of integers representing
Notice that this function expects a list of integers representing
Unicode code points. If you have a list of bytes, you must instead use
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
+1 -1
View File
@@ -3,7 +3,7 @@ defprotocol List.Chars do
The `List.Chars` protocol is responsible for
converting a structure to a charlist (only if applicable).
The only function that must be implemented is
The only function required to be implemented is
`to_charlist/1` which does the conversion.
The `to_charlist/1` function automatically imported
+12 -17
View File
@@ -14,7 +14,7 @@ defmodule Macro do
end
def fun_inspect(value) do
IO.inspect(value)
IO.inpect(value)
value
end
end
@@ -32,7 +32,7 @@ defmodule Macro do
#=> 1
So far they behave the same, as we are passing an integer as argument.
But what happens when we pass an expression:
But what happens when we pass an expresion:
macro_inspect(1 + 2)
#=> {:+, [line: 3], [1, 2]}
@@ -173,7 +173,6 @@ defmodule Macro do
* `:end_of_expression` - denotes when the end of expression effectively
happens. Available for all expressions except the last one inside a
`__block__` (when `:token_metadata` is true)
* `:indentation` - indentation of a sigil heredoc
The following metadata keys are private:
@@ -533,7 +532,7 @@ defmodule Macro do
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,
MD5 of the module byte code, 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.
@@ -576,7 +575,7 @@ defmodule Macro do
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
not have been compiled. This function is even capable of
expanding structs defined under the module being compiled.
It will raise `CompileError` if the struct is not available.
@@ -896,13 +895,13 @@ defmodule Macro do
end
# All other calls
def to_string({{:., _, [left, _]} = target, meta, []} = ast, fun) do
to_string = call_to_string(target, fun)
def to_string({target, meta, []} = ast, fun) do
target = call_to_string(target, fun)
if is_tuple(left) && meta[:no_parens] do
fun.(ast, to_string)
if meta[:no_parens] do
fun.(ast, target)
else
fun.(ast, to_string <> "()")
fun.(ast, target <> "()")
end
end
@@ -963,14 +962,10 @@ defmodule Macro do
Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)
end
defp bitpart_to_string({:"::", meta, [left, right]} = ast, fun) do
defp bitpart_to_string({:"::", _, [left, right]} = ast, fun) do
result =
if meta[:inferred_bitstring_spec] do
to_string(left, fun)
else
op_to_string(left, fun, :"::", :left) <>
"::" <> bitmods_to_string(right, fun, :"::", :right)
end
op_to_string(left, fun, :"::", :left) <>
"::" <> bitmods_to_string(right, fun, :"::", :right)
fun.(ast, result)
end
+45 -68
View File
@@ -37,19 +37,15 @@ defmodule Map do
iex> map["non_existing_key"]
nil
To access atom keys, one may also use the `map.key` notation. Note that `map.key`
will raise a `KeyError` if the `map` doesn't contain the key `:key`, compared to
`map[:key]`, that would return `nil`.
For accessing atom keys, one may also `map.key`. Note that while `map[key]` will
return `nil` if `map` doesn't contain `key`, `map.key` will raise if `map` doesn't
contain the key `:key`.
map = %{foo: "bar", baz: "bong"}
map.foo
#=> "bar"
map.non_existing_key
#=> ** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
> Note: do not add parens when accessing fields, such as in `data.key()`.
> If parenthesis are used, Elixir will consider it to be a function call
> on `data`, which would be expected to be an atom.
iex> map = %{foo: "bar", baz: "bong"}
iex> map.foo
"bar"
iex> map.non_existing_key
** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
The two syntaxes for accessing keys reveal the dual nature of maps. The `map[key]`
syntax is used for dynamically created maps that may have any key, of any type.
@@ -67,10 +63,8 @@ defmodule Map do
iex> %{a: a} = %{:a => 1, "b" => 2, [:c, :e, :e] => 3}
iex> a
1
But this will raise a `MatchError` exception:
%{:c => 3} = %{:a => 1, 2 => :b}
iex> %{:c => 3} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
Variables can be used as map keys both when writing map literals as well as
when matching:
@@ -88,10 +82,8 @@ defmodule Map do
iex> map = %{one: 1, two: 2}
iex> %{map | one: "one"}
%{one: "one", two: 2}
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
%{map | three: 3}
iex> %{map | three: 3}
** (KeyError) key :three not found
The functions in this module that need to find a specific key work in logarithmic time.
This means that the time it takes to find keys grows as the map grows, but it's not
@@ -244,8 +236,8 @@ defmodule Map do
@doc """
Fetches the value for a specific `key` in the given `map`.
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
If `map` doesn't contain `key`, `:error` is returned.
If `map` contains the given `key` with value `value`, then `{:ok, value}` is
returned. If `map` doesn't contain `key`, `:error` is returned.
Inlined by the compiler.
@@ -264,7 +256,7 @@ defmodule Map do
Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`.
If `map` contains `key`, the corresponding value is returned. If
If `map` contains the given `key`, the corresponding value is returned. If
`map` doesn't contain `key`, a `KeyError` exception is raised.
Inlined by the compiler.
@@ -306,20 +298,8 @@ defmodule Map do
end
end
@doc """
Puts a value under `key` only if the `key` already exists in `map`.
## Examples
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace(%{a: 1}, :b, 2)
%{a: 1}
"""
@doc since: "1.11.0"
@spec replace(map, key, value) :: map
@doc false
@deprecated "Use Map.fetch/2 + Map.put/3 instead"
def replace(map, key, value) do
case map do
%{^key => _value} ->
@@ -334,7 +314,8 @@ defmodule Map do
end
@doc """
Puts a value under `key` only if the `key` already exists in `map`.
Alters the value stored under `key` to `value`, but only
if the entry `key` already exists in `map`.
If `key` is not present in `map`, a `KeyError` exception is raised.
@@ -439,7 +420,7 @@ defmodule Map do
@doc """
Gets the value for a specific `key` in `map`.
If `key` is present in `map` then its value `value` is
If `key` is present in `map` with value `value`, then `value` is
returned. Otherwise, `default` is returned.
If `default` is not provided, `nil` is used.
@@ -473,7 +454,7 @@ defmodule Map do
@doc """
Gets the value for a specific `key` in `map`.
If `key` is present in `map` then its value `value` is
If `key` is present in `map` with value `value`, then `value` is
returned. Otherwise, `fun` is evaluated and its result is returned.
This is useful if the default value is very expensive to calculate or
@@ -601,39 +582,38 @@ defmodule Map do
@doc """
Updates the `key` in `map` with the given function.
If `key` is present in `map` then the existing value is passed to `fun` and its result is
used as the updated value of `key`. If `key` is
not present in `map`, `default` is inserted as the value of `key`. The default
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
not present in `map`, `initial` is inserted as the value of `key`. The initial
value will not be passed through the update function.
## Examples
iex> Map.update(%{a: 1}, :a, 13, fn existing_value -> existing_value * 2 end)
iex> Map.update(%{a: 1}, :a, 13, &(&1 * 2))
%{a: 2}
iex> Map.update(%{a: 1}, :b, 11, fn existing_value -> existing_value * 2 end)
iex> Map.update(%{a: 1}, :b, 11, &(&1 * 2))
%{a: 1, b: 11}
"""
@spec update(map, key, default :: value, (existing_value :: value -> updated_value :: value)) ::
map
def update(map, key, default, fun) when is_function(fun, 1) do
@spec update(map, key, value, (value -> value)) :: map
def update(map, key, initial, fun) when is_function(fun, 1) do
case map do
%{^key => value} ->
put(map, key, fun.(value))
%{} ->
put(map, key, default)
put(map, key, initial)
other ->
:erlang.error({:badmap, other}, [map, key, default, fun])
:erlang.error({:badmap, other}, [map, key, initial, fun])
end
end
@doc """
Removes the value associated with `key` in `map` and returns the value and the updated map.
Returns and removes the value associated with `key` in `map`.
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
the key and `new_map` is the result of removing `key` from `map`. If `key`
If `key` is present in `map` with value `value`, `{value, new_map}` is
returned where `new_map` is the result of removing `key` from `map`. If `key`
is not present in `map`, `{default, map}` is returned.
## Examples
@@ -646,7 +626,7 @@ defmodule Map do
{3, %{a: 1}}
"""
@spec pop(map, key, value) :: {value, new_map :: map}
@spec pop(map, key, value) :: {value, map}
def pop(map, key, default \\ nil) do
case :maps.take(key, map) do
{_, _} = tuple -> tuple
@@ -682,8 +662,8 @@ defmodule Map do
@doc """
Lazily returns and removes the value associated with `key` in `map`.
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
the key and `new_map` is the result of removing `key` from `map`. If `key`
If `key` is present in `map` with value `value`, `{value, new_map}` is
returned where `new_map` is the result of removing `key` from `map`. If `key`
is not present in `map`, `{fun_result, map}` is returned, where `fun_result`
is the result of applying `fun`.
@@ -799,8 +779,8 @@ defmodule Map do
@doc """
Updates `key` with the given function.
If `key` is present in `map` then the existing value is passed to `fun` and its result is
used as the updated value of `key`. If `key` is
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
not present in `map`, a `KeyError` exception is raised.
## Examples
@@ -812,7 +792,7 @@ defmodule Map do
** (KeyError) key :b not found in: %{a: 1}
"""
@spec update!(map, key, (existing_value :: value -> updated_value :: value)) :: map
@spec update!(map, key, (value -> value)) :: map
def update!(map, key, fun) when is_function(fun, 1) do
value = fetch!(map, key)
put(map, key, fun.(value))
@@ -822,13 +802,13 @@ defmodule Map do
Gets the value from `key` and updates it, all in one pass.
`fun` is called with the current value under `key` in `map` (or `nil` if `key`
is not present in `map`) and must return a two-element tuple: the current value
is not present in `map`) and must return a two-element tuple: the "get" value
(the retrieved value, which can be operated on before being returned) and the
new value to be stored under `key` in the resulting new map. `fun` may also
return `:pop`, which means the current value shall be removed from `map` and
returned (making this function behave like `Map.pop(map, key)`).
The returned value is a two-element tuple with the current value returned by
The returned value is a tuple with the "get" value returned by
`fun` and a new map with the updated value under `key`.
## Examples
@@ -850,9 +830,7 @@ defmodule Map do
{nil, %{a: 1}}
"""
@spec get_and_update(map, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, map}
when current_value: value
@spec get_and_update(map, key, (value -> {get, value} | :pop)) :: {get, map} when get: term
def get_and_update(map, key, fun) when is_function(fun, 1) do
current = get(map, key)
@@ -869,7 +847,7 @@ defmodule Map do
end
@doc """
Gets the value from `key` and updates it, all in one pass. Raises if there is no `key`.
Gets the value from `key` and updates it. Raises if there is no `key`.
Behaves exactly like `get_and_update/3`, but raises a `KeyError` exception if
`key` is not present in `map`.
@@ -892,9 +870,8 @@ defmodule Map do
{1, %{}}
"""
@spec get_and_update!(map, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, map}
when current_value: value
@spec get_and_update!(map, key, (value -> {get, value} | :pop)) :: {get, map}
when get: term
def get_and_update!(map, key, fun) when is_function(fun, 1) do
value = fetch!(map, key)
+1 -1
View File
@@ -238,7 +238,7 @@ defmodule MapSet do
Map.equal?(map1, map2)
end
# Elixir v1.5 changed the map representation, so on
# Elixir v1.5 change the map representation, so on
# version mismatch we need to compare the keys directly.
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
map_size(map1) == map_size(map2) and all_in?(map1, map2)
+21 -41
View File
@@ -57,7 +57,7 @@ defmodule Module do
one of the callbacks, a warning will be raised.
For detailed documentation, see the
[behaviour typespec documentation](typespecs.md#behaviours).
[behaviour typespec documentation](typespecs.html#behaviours).
### `@impl`
@@ -248,13 +248,8 @@ defmodule Module do
attribute allows the module to annotate which external resources
have been used.
Tools may use this information to ensure the module is recompiled
in case any of the external resources change, see for example:
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
If the external resource does not exist, the module still has
a dependency on it, causing the module be recompiled as soon
as the file is added.
Tools like Mix may use this information to ensure the module is
recompiled in case any of the external resources change.
### `@file`
@@ -350,8 +345,6 @@ defmodule Module do
behaviour callbacks are optional
* `@impl` - declares an implementation of a callback function or macro
For detailed documentation, see the [typespec documentation](typespecs.md).
### Custom attributes
In addition to the built-in attributes outlined above, custom attributes may
@@ -506,6 +499,8 @@ 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
You can see a handful more options used by the Erlang compiler in
the documentation for the [`:compile` module](http://www.erlang.org/doc/man/compile.html).
'''
@typep definition :: {atom, arity}
@@ -630,7 +625,7 @@ defmodule Module do
It returns a tuple of shape `{:module, module, binary, term}`
where `module` is the module name, `binary` is the module
bytecode and `term` is the result of the last expression in
byte code and `term` is the result of the last expression in
`quoted`.
Similar to `Kernel.defmodule/2`, the binary will only be
@@ -1054,11 +1049,11 @@ defmodule Module do
"""
@spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, kind)
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
def definitions_in(module, def_kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
assert_not_compiled!(__ENV__.function, module, @extra_error_msg_definitions_in)
{set, _} = data_tables_for(module)
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
:lists.concat(:ets.match(set, {{:def, :"$1"}, def_kind, :_, :_, :_, :_}))
end
@doc """
@@ -1402,7 +1397,7 @@ defmodule Module do
if doc, do: {:error, :private_doc}, else: :ok
else
{set, _bag} = data_tables_for(module)
compile_doc(set, nil, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
compile_doc(set, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
:ok
end
end
@@ -1415,17 +1410,16 @@ defmodule Module do
{set, bag} = data_tables_for(module)
{arity, defaults} = args_count(args, 0, 0)
context = Keyword.get(:ets.lookup_element(set, {:def, {name, arity}}, 3), :context)
impl = compile_impl(set, bag, context, name, env, kind, arity, defaults)
impl = compile_impl(set, bag, name, env, kind, arity, defaults)
doc_meta = compile_doc_meta(set, bag, name, arity, defaults)
{line, doc} = get_doc_info(set, env)
compile_doc(set, context, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
compile_doc(set, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
:ok
end
defp compile_doc(_table, _ctx, line, kind, name, arity, _args, _body, doc, _meta, env, _impl)
defp compile_doc(_table, line, kind, name, arity, _args, _body, doc, _doc_meta, env, _impl)
when kind in [:defp, :defmacrop] do
if doc do
message =
@@ -1436,37 +1430,21 @@ defmodule Module do
end
end
defp compile_doc(table, ctx, line, kind, name, arity, args, body, doc, doc_meta, env, impl) do
defp compile_doc(table, line, kind, name, arity, args, _body, doc, doc_meta, env, impl) do
key = {doc_key(kind), name, arity}
signature = build_signature(args, env)
case :ets.lookup(table, key) do
[] ->
doc = if is_nil(doc) && impl, do: false, else: doc
:ets.insert(table, {key, ctx, line, signature, doc, doc_meta})
[{_, current_ctx, current_line, current_sign, current_doc, current_doc_meta}] ->
if is_binary(current_doc) and is_binary(doc) and body != nil and is_nil(current_ctx) do
message = ~s'''
redefining @doc attribute previously set at line #{current_line}.
Please remove the duplicate docs. If instead you want to override a \
previously defined @doc, attach the @doc attribute to a function head:
@doc """
new docs
"""
def #{name}(...)
'''
IO.warn(message, Macro.Env.stacktrace(%{env | line: line}))
end
:ets.insert(table, {key, line, signature, doc, doc_meta})
[{_, current_line, current_sign, current_doc, current_doc_meta}] ->
signature = merge_signatures(current_sign, signature, 1)
doc = if is_nil(doc), do: current_doc, else: doc
doc = if is_nil(doc) && impl, do: false, else: doc
doc_meta = Map.merge(current_doc_meta, doc_meta)
:ets.insert(table, {key, ctx, current_line, signature, doc, doc_meta})
:ets.insert(table, {key, current_line, signature, doc, doc_meta})
end
end
@@ -1514,12 +1492,14 @@ defmodule Module do
defp deprecated_reason(name, arity, reason),
do: {:deprecated, {{name, arity}, reason}}
defp compile_impl(set, bag, context, name, env, kind, arity, defaults) do
defp compile_impl(set, bag, name, env, kind, arity, defaults) do
%{line: line, file: file} = env
case :ets.take(set, :impl) do
[{:impl, value, _}] ->
impl = {{name, arity}, context, defaults, kind, line, file, value}
pair = {name, arity}
meta = :ets.lookup_element(set, {:def, pair}, 3)
impl = {pair, Keyword.get(meta, :context), defaults, kind, line, file, value}
:ets.insert(bag, {:impls, impl})
value
+317
View File
@@ -0,0 +1,317 @@
defmodule Module.Checker do
alias Module.ParallelChecker
@moduledoc false
def verify(module, cache) do
case prepare_module(module) do
{:ok, map} ->
undefined_and_deprecation_warnings = undefined_and_deprecation_warnings(map, cache)
infer_warnings = infer_definitions(map)
warnings = infer_warnings ++ undefined_and_deprecation_warnings
emit_warnings(warnings)
:error ->
[]
end
end
defp prepare_module({module, module_map}) when is_map(module_map) do
{:ok,
%{
module: module,
file: module_map.file,
definitions: module_map.definitions,
deprecated: module_map.deprecated,
no_warn_undefined: no_warn_undefined(module_map.compile_opts)
}}
end
defp prepare_module({module, binary}) when is_binary(binary) do
with {:ok, debug_info} <- debug_info(module, binary),
{:ok, checker_info} <- checker_chunk(binary) do
{:ok,
%{
module: module,
file: debug_info.file,
definitions: debug_info.definitions,
deprecated: checker_info.deprecated,
no_warn_undefined: checker_info.no_warn_undefined
}}
end
end
defp no_warn_undefined(compile_opts) do
for(
{:no_warn_undefined, values} <- compile_opts,
value <- List.wrap(values),
do: value
)
end
defp debug_info(module, binary) do
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} <- chunk,
{:ok, info} <- backend.debug_info(:elixir_v1, module, data, []) do
{:ok, %{definitions: info.definitions, file: info.relative_file}}
else
_ -> :error
end
end
defp checker_chunk(binary) do
with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
deprecated = Enum.map(contents.exports, fn {fun, map} -> {fun, map.deprecated_reason} end)
{:ok, %{deprecated: deprecated, no_warn_undefined: contents.no_warn_undefined}}
else
_ -> :error
end
end
defp infer_definitions(map) do
results = Module.Types.infer_definitions(map.file, map.module, map.definitions)
Enum.flat_map(results, fn {_function, reasons} -> reasons end)
end
defp undefined_and_deprecation_warnings(map, cache) do
state = %{
cache: cache,
file: map.file,
module: map.module,
no_warn_undefined: merge_no_warn_undefined(map),
function: nil,
warnings: []
}
state = check_definitions(map.definitions, state)
state.warnings
|> merge_warnings()
|> sort_warnings()
end
defp merge_no_warn_undefined(map) do
case Code.get_compiler_option(:no_warn_undefined) do
:all ->
:all
list when is_list(list) ->
map.no_warn_undefined ++ list
end
end
defp check_definitions(definitions, state) do
Enum.reduce(definitions, state, &check_definition/2)
end
defp check_definition({function, _kind, meta, clauses}, state) do
with_file_meta(%{state | function: function}, meta, fn state ->
Enum.reduce(clauses, state, &check_clause/2)
end)
end
defp with_file_meta(%{file: original_file} = state, meta, fun) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} ->
state = fun.(%{state | file: meta_file})
%{state | file: original_file}
:error ->
fun.(state)
end
end
defp check_clause({_meta, args, _guards, body}, state) do
state = check_expr(args, state)
check_expr(body, state)
end
# &Mod.fun/arity
defp check_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
when is_atom(module) and is_atom(fun) do
check_remote(module, fun, arity, meta, state)
end
# Mod.fun(...)
defp check_expr({{:., meta, [module, fun]}, _, args}, state)
when is_atom(module) and is_atom(fun) do
state = check_remote(module, fun, length(args), meta, state)
check_expr(args, state)
end
# %Module{...}
defp check_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
when is_atom(module) and is_list(args) do
state = check_remote(module, :__struct__, 0, meta, state)
check_expr(args, state)
end
# Function call
defp check_expr({left, _meta, right}, state) when is_list(right) do
state = check_expr(right, state)
check_expr(left, state)
end
# {x, y}
defp check_expr({left, right}, state) do
state = check_expr(right, state)
check_expr(left, state)
end
# [...]
defp check_expr(list, state) when is_list(list) do
Enum.reduce(list, state, &check_expr/2)
end
defp check_expr(_other, state) do
state
end
defp check_remote(module, fun, arity, meta, state) do
# TODO: In the future we may want to warn for modules defined
# in the local context
if Keyword.get(meta, :context_module, false) and state.module != module do
state
else
ParallelChecker.preload_module(state.cache, module)
check_export(module, fun, arity, meta, state)
end
end
defp check_export(module, fun, arity, meta, state) do
case ParallelChecker.fetch_export(state.cache, module, fun, arity) do
{:ok, :def, reason} ->
check_deprecated(module, fun, arity, reason, meta, state)
{:ok, :defmacro, reason} ->
state = warn(meta, state, {:unrequired_module, module, fun, arity})
check_deprecated(module, fun, arity, reason, meta, state)
{:error, :module} ->
if warn_undefined?(module, fun, arity, state) do
warn(meta, state, {:undefined_module, module, fun, arity})
else
state
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, state) do
exports = ParallelChecker.all_exports(state.cache, module)
warn(meta, state, {:undefined_function, module, fun, arity, exports})
else
state
end
end
end
defp check_deprecated(module, fun, arity, reason, meta, state) do
if reason do
warn(meta, state, {:deprecated, module, fun, arity, reason})
else
state
end
end
# TODO: Do not warn inside guards
# TODO: Properly handle protocols
defp warn_undefined?(_module, :__impl__, 1, _state), do: false
defp warn_undefined?(_module, :module_info, 0, _state), do: false
defp warn_undefined?(_module, :module_info, 1, _state), do: false
defp warn_undefined?(:erlang, :orelse, 2, _state), do: false
defp warn_undefined?(:erlang, :andalso, 2, _state), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, state) do
not Enum.any?(state.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
defp warn(meta, state, warning) do
{fun, arity} = state.function
location = {state.file, meta[:line] || 0, {state.module, fun, arity}}
%{state | warnings: [{__MODULE__, warning, location} | state.warnings]}
end
defp merge_warnings(warnings) do
Enum.reduce(warnings, %{}, fn {module, warning, location}, acc ->
locations = MapSet.new([location])
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
end)
end
defp sort_warnings(warnings) do
warnings
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|> Enum.sort()
end
defp emit_warnings(warnings) do
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
print_warning([message, ?\n, format_locations(locations)])
Enum.map(locations, fn {file, line, _mfa} ->
{file, line, message}
end)
end)
end
def format_warning({:undefined_module, module, fun, arity}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_warning({:undefined_function, module, fun, arity, exports}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]
end
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_warning({:unrequired_module, module, fun, arity}) do
[
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]
end
defp format_locations([location]) do
format_location(location)
end
defp format_locations(locations) do
[
"Found at #{length(locations)} locations:\n",
Enum.map(locations, &format_location/1)
]
end
defp format_location({file, line, {module, fun, arity}}) do
file = Path.relative_to_cwd(file)
line = if line > 0, do: [Integer.to_string(line), ": "], else: []
mfa = Exception.format_mfa(module, fun, arity)
[" ", file, ?:, line, mfa, ?\n]
end
defp print_warning(message) do
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
end
end
+1 -1
View File
@@ -137,7 +137,7 @@ defmodule Module.LocalsTracker do
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private macros are unreachable unless they have been
# All private micros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
end
+12 -133
View File
@@ -10,24 +10,17 @@ defmodule Module.ParallelChecker do
the modules and adds the ExCk chunk to the binaries. Returns the updated
binaries and a list of warnings from the verification.
"""
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer() | nil) :: [warning()]
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer()) :: [warning()]
def verify(compiled_modules, runtime_binaries, schedulers \\ nil) do
compiled_maps = Enum.map(compiled_modules, fn {map, _binary} -> {map.module, map} end)
check_modules = compiled_maps ++ runtime_binaries
case compiled_maps ++ runtime_binaries do
[] ->
[]
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
{:ok, server} = :gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
preload_cache(get_ets(server), check_modules)
start(server)
check_modules ->
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
{:ok, server} =
:gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
preload_cache(get_ets(server), check_modules)
start(server)
collect_results(length(check_modules), [])
end
collect_results(length(check_modules), [])
end
defp collect_results(0, warnings) do
@@ -41,14 +34,6 @@ defmodule Module.ParallelChecker do
end
end
@doc """
Test cache.
"""
def test_cache do
{:ok, pid} = :gen_server.start_link(__MODULE__, [[], self(), 1], [])
{pid, get_ets(pid)}
end
@doc """
Preloads a module into the cache. Call this function before any other
cache lookups for the module.
@@ -96,116 +81,6 @@ defmodule Module.ParallelChecker do
|> Enum.sort()
end
## Module checking
defp check_module(module, cache) do
case extract_definitions(module) do
{:ok, module, file, definitions, no_warn_undefined} ->
Module.Types.warnings(module, file, definitions, no_warn_undefined, cache)
|> group_warnings()
|> emit_warnings()
:error ->
[]
end
end
defp extract_definitions({module, module_map}) when is_map(module_map) do
no_warn_undefined =
module_map.compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
{:ok, module, module_map.file, module_map.definitions, no_warn_undefined}
end
defp extract_definitions({module, binary}) when is_binary(binary) do
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
extract_definitions({module, module_map})
else
_ -> :error
end
end
defp extract_no_warn_undefined(compile_opts) do
for(
{:no_warn_undefined, values} <- compile_opts,
value <- List.wrap(values),
do: value
)
end
defp merge_compiler_no_warn_undefined(no_warn_undefined) do
case Code.get_compiler_option(:no_warn_undefined) do
:all ->
:all
list when is_list(list) ->
no_warn_undefined ++ list
end
end
## Warning helpers
def group_warnings(warnings) do
warnings
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
locations = MapSet.new([location])
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
end)
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|> Enum.sort()
end
def emit_warnings(warnings) do
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
print_warning([message, ?\n, format_locations(locations)])
Enum.map(locations, fn {file, line, _mfa} ->
{file, line, message}
end)
end)
end
defp format_locations([location]) do
format_location(location)
end
defp format_locations(locations) do
[
"Found at #{length(locations)} locations:\n",
Enum.map(locations, &format_location/1)
]
end
defp format_location({file, line, {module, fun, arity}}) do
mfa = Exception.format_mfa(module, fun, arity)
[format_file_line(file, line), ": ", mfa, ?\n]
end
defp format_location({file, line, nil}) do
[format_file_line(file, line), ?\n]
end
defp format_location({file, line, module}) do
[format_file_line(file, line), ": ", inspect(module), ?\n]
end
defp format_file_line(file, line) do
file = Path.relative_to_cwd(file)
line = if line > 0, do: [?: | Integer.to_string(line)], else: []
[" ", file, line]
end
defp print_warning(message) do
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
end
## Server callbacks
def init([modules, send_results, schedulers]) do
ets = :ets.new(:checker_cache, [:set, :public, {:read_concurrency, true}])
@@ -244,6 +119,10 @@ defmodule Module.ParallelChecker do
{:reply, ets, state}
end
def handle_cast(:start, %{modules: []} = state) do
{:stop, :normal, state}
end
def handle_cast(:start, state) do
{:noreply, spawn_checkers(state)}
end
@@ -297,7 +176,7 @@ defmodule Module.ParallelChecker do
send_results_pid = state.send_results
spawn_link(fn ->
warnings = check_module(verify, {parent, ets})
warnings = Module.Checker.verify(verify, {parent, ets})
send(send_results_pid, {__MODULE__, module, warnings})
send(parent, {__MODULE__, :done})
end)
+190 -330
View File
@@ -1,75 +1,69 @@
defmodule Module.Types do
@moduledoc false
defmodule Error do
defexception [:message]
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern}
@doc """
Infer function definitions' types.
"""
def infer_definitions(file, module, defs) do
clauses = infer_signatures(file, module, defs)
infer_bodies(clauses)
end
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern, Unify}
defp infer_signatures(file, module, defs) do
Enum.map(defs, fn {{fun, _arity} = function, kind, meta, clauses} ->
stack = head_stack()
context = head_context(file, module, function)
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
stack = stack()
clauses =
Enum.map(clauses, fn {_meta, params, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], params})]}
stack = push_expr_stack(def_expr, stack)
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
case of_head(params, guards, stack, context) do
{:ok, _signature, context} -> {:ok, {context, body}}
{:error, reason} -> {:error, reason}
end
end)
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
try do
warnings_from_clause(args, guards, body, def_expr, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
error =
Error.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}
#{Macro.to_string(def_expr)}
Please report this bug: https://github.com/elixir-lang/elixir/issues
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
""")
reraise error, __STACKTRACE__
end
end)
{function, clauses}
end)
end
defp with_file_meta(meta, file) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> meta_file
:error -> file
end
end
defp infer_bodies(clauses) do
Enum.map(clauses, fn {function, clauses} ->
errors =
Enum.flat_map(clauses, fn
{:ok, {head_context, body}} ->
stack = body_stack()
context = body_context(head_context)
defp guards_to_expr([], left) do
left
end
case Expr.of_expr(body, stack, context) do
{:ok, _type, _context} -> []
{:error, reason} -> [reason]
end
defp guards_to_expr([guard | guards], left) do
guards_to_expr(guards, {:when, [], [left, guard]})
end
{:error, reason} ->
[reason]
end)
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
head_stack = Unify.push_expr_stack(def_expr, stack)
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
context.warnings
else
{:error, {type, error, context}} ->
[error_to_warning(type, error, context) | context.warnings]
end
{function, errors}
end)
end
@doc false
def context(file, module, function, no_warn_undefined, cache) do
def of_head(params, guards, stack, context) do
with {:ok, types, context} <-
map_reduce_ok(params, context, &Pattern.of_pattern(&1, stack, &2)),
# TODO: Check that of_guard/3 returns boolean() | :fail
{:ok, _, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
do: {:ok, lift_types(types, context), context}
end
@doc false
def head_context(file, module, function) do
%{
# File of module
file: file,
@@ -77,10 +71,6 @@ defmodule Module.Types do
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined
no_warn_undefined: no_warn_undefined,
# A list of cached modules received from the parallel compiler
cache: cache,
# Expression variable to type variable
vars: %{},
# Type variable to expression variable
@@ -95,23 +85,18 @@ defmodule Module.Types do
# Track if a variable was infered from a type guard function such is_tuple/1
# or a guard function that fails such as elem/2, possible values are:
# `:guarded` when `is_tuple(x)`
# `:guarded` when `is_tuple and elem(x, 0)`
# `:fail` when `elem(x, 0)`
guard_sources: %{},
# A list with all warnings from the running the code
warnings: []
# `:guarded_fail` when `is_tuple and elem(x, 0)`
guard_sources: %{}
}
end
@doc false
def stack() do
def head_stack() do
%{
# Stack of variables we have refined during unification,
# used for creating relevant traces
unify_stack: [],
# Last expression we have recursed through during inference,
# Stack of expression we have recursed through during inference,
# used for tracing
last_expr: nil,
expr_stack: [],
# When false do not add a trace when a type variable is refined,
# useful when merging contexts where the variables already have traces
trace: true,
@@ -120,11 +105,47 @@ defmodule Module.Types do
type_guards_enabled?: true,
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: nil
context: :pattern
}
end
## VARIABLE LIFTING
@doc false
def body_context(head_context) do
%{
# File of module
file: head_context.file,
# Module of definitions
module: head_context.module,
# Current function
function: head_context.function,
# Expression variable to type variable
vars: head_context.vars,
# Type variable to expression variable
types_to_vars: head_context.types_to_vars,
# Type variable to type
types: head_context.types,
# Trace of all variables that have been refined to a type,
# including the type they were refined to, why, and where
traces: head_context.traces,
# Counter to give type variables unique names
counter: head_context.counter
}
end
@doc false
def body_stack() do
%{
# Stack of expression we have recursed through during inference,
# used for tracing
expr_stack: [],
# When false do not add a trace when a type variable is refined,
# useful when merging contexts where the variables already have traces
trace: true,
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: :expr
}
end
@doc """
Lifts type variables to their infered types from the context.
@@ -140,9 +161,7 @@ defmodule Module.Types do
types
end
@doc """
Lifts a single type to its infered type from the context.
"""
@doc false
def lift_type(type, context) do
context = %{
types: context.types,
@@ -154,6 +173,28 @@ defmodule Module.Types do
type
end
## GUARDS
# TODO: Remove this and let multiple when be treated as multiple clauses,
# meaning they will be intersection types
defp guards_to_or([]) do
[]
end
defp guards_to_or(guards) do
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
end
defp guards_to_expr([], left) do
left
end
defp guards_to_expr([guard | guards], left) do
guards_to_expr(guards, {:when, [], [left, guard]})
end
## VARIABLE LIFTING
# Lift type variable to its infered (hopefully concrete) types from the context
defp do_lift_type({:var, var}, context) do
case Map.fetch(context.lifted_types, var) do
@@ -179,17 +220,17 @@ defmodule Module.Types do
end
end
defp do_lift_type({:tuple, n, types}, context) do
defp do_lift_type({:tuple, types}, context) do
{types, context} = Enum.map_reduce(types, context, &do_lift_type/2)
{{:tuple, n, types}, context}
{{:tuple, types}, context}
end
defp do_lift_type({:map, pairs}, context) do
{pairs, context} =
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
Enum.map_reduce(pairs, context, fn {key, value}, context ->
{key, context} = do_lift_type(key, context)
{value, context} = do_lift_type(value, context)
{{kind, key, value}, context}
{{key, value}, context}
end)
{{:map, pairs}, context}
@@ -213,158 +254,54 @@ defmodule Module.Types do
{type, context}
end
## ERROR TO WARNING
## ERROR FORMATTING
# Collect relevant information from context and traces to report error
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
line = get_meta(stack.last_expr)[:line]
location = {context.file, line, {context.module, fun, arity}}
traces = type_traces(stack, context)
traces = tag_traces(traces, context)
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
{Module.Types, error, location}
def format_warning({:unable_unify, left, right, expr, traces}) do
[
"incompatible types:\n\n ",
format_type(left),
" !~ ",
format_type(right),
"\n\n",
format_expr(expr),
format_traces(traces),
"Conflict found at"
]
end
# Collect relevant traces from context.traces using stack.unify_stack
defp type_traces(stack, context) do
# TODO: Do we need the unify_stack or is enough to only get the last variable
# in the stack since we get related variables anyway?
stack =
stack.unify_stack
|> Enum.uniq()
|> Enum.flat_map(&[&1 | related_variables(&1, context.types)])
|> Enum.uniq()
Enum.flat_map(stack, fn var_index ->
with %{^var_index => traces} <- context.traces,
%{^var_index => expr_var} <- context.types_to_vars do
Enum.map(traces, &{expr_var, &1})
else
_other -> []
end
end)
defp format_expr(nil) do
[]
end
defp related_variables(var, types) do
Enum.flat_map(types, fn
{related_var, {:var, ^var}} ->
[related_var | related_variables(related_var, types)]
_ ->
[]
end)
defp format_expr(expr) do
[
"in expression:\n\n ",
expr_to_string(expr),
"\n\n"
]
end
# Tag if trace is for a concrete type or type variable
defp tag_traces(traces, context) do
Enum.flat_map(traces, fn {var, {type, expr, location}} ->
with {:var, var_index} <- type,
%{^var_index => expr_var} <- context.types_to_vars do
[{var, {:var, expr_var, expr, location}}]
else
_ -> [{var, {:type, type, expr, location}}]
end
end)
defp format_traces([]) do
[]
end
## FORMAT WARNINGS
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
cond do
map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) ->
{:ok, atom, known_atoms} = missing_field(left, right)
# Drop the last trace which is the expression map.foo
traces = Enum.drop(traces, 1)
{traces, hints} = format_traces(traces, true)
defp format_traces(traces) do
Enum.map(traces, fn
{var, {:type, type, expr, location}} ->
[
"undefined field \"#{atom}\" ",
format_expr(expr, location),
"expected one of the following fields: ",
Enum.map_join(Enum.sort(known_atoms), ", ", & &1),
"\n\n",
traces,
format_message_hints(hints),
"Conflict found at"
]
true ->
simplify_left? = simplify_type?(left, right)
simplify_right? = simplify_type?(right, left)
{traces, hints} = format_traces(traces, simplify_left? or simplify_right?)
[
"incompatible types:\n\n ",
Unify.format_type(left, simplify_left?),
" !~ ",
Unify.format_type(right, simplify_right?),
"\n\n",
format_expr(expr, location),
traces,
format_message_hints(hints),
"Conflict found at"
]
end
end
defp missing_field(
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
{:map, fields}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(
{:map, fields},
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(_, _), do: :error
defp matched_missing_field(fields, type, atom) do
if List.keymember?(fields, type, 1) do
:error
else
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
{:ok, atom, known_atoms}
end
end
defp format_traces([], _simplify?) do
{[], []}
end
defp format_traces(traces, simplify?) do
traces
|> Enum.reverse()
|> Enum.map_reduce([], fn
{var, {:type, type, expr, location}}, hints ->
{hint, hints} = format_type_hint(type, expr, hints)
trace = [
"where \"",
Macro.to_string(var),
"\" was given the type ",
Unify.format_type(type, simplify?),
hint,
Module.Types.format_type(type),
" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
expr_to_string(expr),
"\n\n"
]
{trace, hints}
{var1, {:var, var2, expr, location}}, hints ->
trace = [
{var1, {:var, var2, expr, location}} ->
[
"where \"",
Macro.to_string(var1),
"\" was given the same type as \"",
@@ -372,42 +309,57 @@ defmodule Module.Types do
"\" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
expr_to_string(expr),
"\n\n"
]
{trace, hints}
end)
end
defp format_location({file, line, _mfa}) do
format_location({file, line})
end
defp format_location({file, line}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line)], else: []
[file, ?:, line, ?\n]
end
defp simplify_type?(type, other) do
map_type?(type) and not map_type?(other)
@doc false
def format_type({:union, types}) do
"#{Enum.map_join(types, " | ", &format_type/1)}"
end
## EXPRESSION FORMATTING
defp format_expr(nil, _location) do
[]
def format_type({:tuple, types}) do
"{#{Enum.map_join(types, ", ", &format_type/1)}}"
end
defp format_expr(expr, location) do
[
"in expression:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
def format_type({:list, type}) do
"[#{format_type(type)}]"
end
def format_type({:map, pairs}) do
case List.keytake(pairs, :__struct__, 0) do
{{:__struct__, struct}, pairs} ->
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
nil ->
"%{#{format_map_pairs(pairs)}}"
end
end
def format_type({:atom, literal}) do
inspect(literal)
end
def format_type(atom) when is_atom(atom) do
"#{atom}()"
end
def format_type({:var, index}) do
"var#{index}"
end
defp format_map_pairs(pairs) do
Enum.map_join(pairs, ", ", fn {left, right} ->
"#{format_type(left)} => #{format_type(right)}"
end)
end
@doc false
@@ -419,109 +371,17 @@ defmodule Module.Types do
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
{:., _, [:erlang, :orelse]} -> :or
{:., _, [:erlang, :andalso]} -> :and
{{:., _, [mod, fun]}, _, args} -> erl_to_ex(mod, fun, args)
other -> other
end)
end
defp erl_to_ex(mod, fun, args, meta) do
defp erl_to_ex(mod, fun, args) 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, [], args}
{mod, fun, args} -> {{:., [], [mod, fun]}, [], args}
end
end
## Hints
defp format_message_hints(hints) do
hints |> Enum.uniq() |> Enum.reverse() |> Enum.map(&format_message_hint/1)
end
defp format_message_hint(:inferred_dot) do
"""
HINT: "var.field" (without parentheses) implies "var" is a map() while \
"var.fun()" (with parentheses) implies "var" is an atom()
"""
end
defp format_message_hint(:inferred_bitstring_spec) do
"""
HINT: all expressions given to binaries are assumed to be of type \
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
to change the default behaviour.
"""
end
defp format_type_hint(type, expr, hints) do
case format_type_hint(type, expr) do
{message, hint} -> {message, [hint | hints]}
:error -> {[], hints}
end
end
defp format_type_hint(type, expr) do
cond do
dynamic_map_dot?(type, expr) ->
{" (due to calling var.field)", :inferred_dot}
dynamic_remote_call?(type, expr) ->
{" (due to calling var.fun())", :inferred_dot}
inferred_bitstring_spec?(type, expr) ->
{[], :inferred_bitstring_spec}
true ->
:error
end
end
defp dynamic_map_dot?(type, expr) do
with true <- map_type?(type),
{{:., _meta1, [_map, _field]}, meta2, []} <- expr,
true <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp dynamic_remote_call?(type, expr) do
with true <- atom_type?(type),
{{:., _meta1, [_module, _field]}, meta2, []} <- expr,
false <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp inferred_bitstring_spec?(type, expr) do
with true <- integer_type?(type),
{:<<>>, _, args} <- expr,
true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do
true
else
_ -> false
end
end
## Formatting helpers
defp indent(string) do
String.replace(string, "\n", " \n")
end
defp map_type?({:map, _}), do: true
defp map_type?(_other), do: false
defp atom_type?(:atom), do: true
defp atom_type?({:atom, _}), do: false
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
defp atom_type?(_other), do: false
defp integer_type?(:integer), do: true
defp integer_type?(_other), do: false
end
+1 -492
View File
@@ -1,498 +1,7 @@
defmodule Module.Types.Expr do
@moduledoc false
alias Module.Types.{Of, Pattern}
import Module.Types.{Helpers, Unify}
def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do
of_expr(expr, %{stack | context: :expr}, context)
end
# :atom
def of_expr(atom, _stack, context) when is_atom(atom) do
{:ok, {:atom, atom}, context}
end
# 12
def of_expr(literal, _stack, context) when is_integer(literal) do
{:ok, :integer, context}
end
# 1.2
def of_expr(literal, _stack, context) when is_float(literal) do
{:ok, :float, context}
end
# "..."
def of_expr(literal, _stack, context) when is_binary(literal) do
{:ok, :binary, context}
end
# #PID<...>
def of_expr(literal, _stack, context) when is_pid(literal) do
def of_expr(_expr, _stack, context) do
{:ok, :dynamic, context}
end
# <<...>>>
def of_expr({:<<>>, _meta, args}, stack, context) do
result = Of.binary(args, stack, context, &of_expr/3)
case result do
{:ok, context} -> {:ok, :binary, context}
{:error, reason} -> {:error, reason}
end
end
# left | []
def of_expr({:|, _meta, [left_expr, []]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
of_expr(left_expr, stack, context)
end
# left | right
def of_expr({:|, _meta, [left_expr, right_expr]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case of_expr(left_expr, stack, context) do
{:ok, left, context} ->
case of_expr(right_expr, stack, context) do
{:ok, {:list, right}, context} ->
{:ok, to_union([left, right], context), context}
{:ok, right, context} ->
{:ok, to_union([left, right], context), context}
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
# []
def of_expr([], _stack, context) do
{:ok, {:list, :dynamic}, context}
end
# [expr, ...]
def of_expr(exprs, stack, context) when is_list(exprs) do
stack = push_expr_stack(exprs, stack)
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
{:error, reason} -> {:error, reason}
end
end
# __CALLER__
def of_expr({:__CALLER__, _meta, var_context}, _stack, context) when is_atom(var_context) do
struct_pair = {:required, {:atom, :__struct__}, {:atom, Macro.Env}}
pairs =
Enum.map(Map.from_struct(Macro.Env.__struct__()), fn {key, _value} ->
{:required, {:atom, key}, :dynamic}
end)
{:ok, {:map, [struct_pair | pairs]}, context}
end
# __STACKTRACE__
def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context) when is_atom(var_context) do
file = {:tuple, 2, [{:atom, :file}, {:list, :integer}]}
line = {:tuple, 2, [{:atom, :line}, :integer]}
file_line = {:list, {:union, [file, line]}}
type = {:list, {:tuple, 4, [:atom, :atom, :integer, file_line]}}
{:ok, type, context}
end
# var
def of_expr(var, _stack, context) when is_var(var) do
{:ok, get_var!(var, context), context}
end
# {left, right}
def of_expr({left, right}, stack, context) do
of_expr({:{}, [], [left, right]}, stack, context)
end
# {...}
def of_expr({:{}, _meta, exprs} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
{:error, reason} -> {:error, reason}
end
end
# left = right
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, left_type, context} <-
Pattern.of_pattern(left_expr, stack, context),
{:ok, right_type, context} <- of_expr(right_expr, stack, context),
do: unify(right_type, left_type, %{stack | context: :pattern}, context)
end
# %{map | ...}
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, map_type, context} <- of_expr(map, stack, context),
{:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/3),
dynamic_value_pairs =
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
# Retrieve map type and overwrite with the new value types from the map update
{:map, pairs} = resolve_var(type, context)
updated_pairs =
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
List.keyreplace(pairs, key, 1, {:required, key, value})
end)
{:ok, {:map, updated_pairs}, context}
end
end
# %Struct{map | ...}
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, struct, context} <- Of.struct(module, meta, context),
{:ok, update, context} <- of_expr(update, stack, context) do
unify(update, struct, stack, context)
end
end
# %{...}
def of_expr({:%{}, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
Of.closed_map(args, stack, context, &of_expr/3)
end
# %Struct{...}
def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, struct, context} <- Of.struct(module, meta1, context),
{:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/3) do
unify(map, struct, stack, context)
end
end
# ()
def of_expr({:__block__, _meta, []}, _stack, context) do
{:ok, {:atom, nil}, context}
end
# (expr; expr)
def of_expr({:__block__, _meta, exprs}, stack, context) do
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context}
{:error, reason} -> {:error, reason}
end
end
# case expr do pat -> expr end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context),
{:ok, context} <- of_clauses(clauses, stack, context),
do: {:ok, :dynamic, context}
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case of_clauses(clauses, stack, context) do
{:ok, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
@try_blocks [:do, :after]
@try_clause_blocks [:catch, :else, :after]
# try do expr end
def of_expr({:try, _meta, [blocks]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{result, context} =
reduce_ok(blocks, context, fn
{:rescue, clauses}, context ->
reduce_ok(clauses, context, fn
{:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc ->
{_type, context} = new_pattern_var(var, context)
with {:ok, context} <- of_expr_context(body, stack, context) do
{:ok, keep_warnings(acc, context)}
end
{:->, _, [[var], body]}, context = acc ->
{_type, context} = new_pattern_var(var, context)
with {:ok, context} <- of_expr_context(body, stack, context) do
{:ok, keep_warnings(acc, context)}
end
end)
{block, body}, context = acc when block in @try_blocks ->
with {:ok, context} <- of_expr_context(body, stack, context) do
{:ok, keep_warnings(acc, context)}
end
{block, clauses}, context when block in @try_clause_blocks ->
of_clauses(clauses, stack, context)
end)
case result do
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# receive do pat -> expr end
def of_expr({:receive, _meta, [blocks]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{result, context} =
reduce_ok(blocks, context, fn
{:do, {:__block__, _, []}}, context ->
{:ok, context}
{:do, clauses}, context ->
of_clauses(clauses, stack, context)
{:after, [{:->, _meta, [head, body]}]}, context = acc ->
with {:ok, _type, context} <- of_expr(head, stack, context),
{:ok, _type, context} <- of_expr(body, stack, context),
do: {:ok, keep_warnings(acc, context)}
end)
case result do
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# for pat <- expr do expr end
def of_expr({:for, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)),
{:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do
if Keyword.has_key?(opts, :reduce) do
with {:ok, context} <- of_clauses(block, stack, context) do
{:ok, :dynamic, context}
end
else
with {:ok, _type, context} <- of_expr(block, stack, context) do
{:ok, :dynamic, context}
end
end
end
end
# with pat <- expr do expr end
def of_expr({:with, _meta, clauses} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do
{:ok, _context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
# fun.(arg)
def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case of_expr(fun, stack, context) do
{:ok, _fun_type, context} ->
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
# expr.key_or_fun
def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, stack, context)
when not is_atom(expr1) do
stack = push_expr_stack(expr2, stack)
if Keyword.get(meta2, :no_parens, false) do
with {:ok, expr_type, context} <- of_expr(expr1, stack, context),
{value_var, context} = add_var(context),
pair_type = {:required, {:atom, key_or_fun}, value_var},
optional_type = {:optional, :dynamic, :dynamic},
map_field_type = {:map, [pair_type, optional_type]},
{:ok, _map_type, context} <- unify(map_field_type, expr_type, stack, context),
do: {:ok, value_var, context}
else
with {:ok, expr_type, context} <- of_expr(expr1, stack, context),
{:ok, _map_type, context} <- unify(expr_type, :atom, stack, context),
do: {:ok, :dynamic, context}
end
end
# expr.fun(arg)
def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, stack, context) do
context = Of.remote(expr1, fun, length(args), meta1, context)
stack = push_expr_stack(expr2, stack)
with {:ok, _expr_type, context} <- of_expr(expr1, stack, context),
{:ok, _fun_type, context} <- of_expr(fun, stack, context) do
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
end
# &Foo.bar/1
def of_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, _stack, context)
when is_atom(module) and is_atom(fun) do
context = Of.remote(module, fun, arity, meta, context)
{:ok, :dynamic, context}
end
# &foo/1
# & &1
def of_expr({:&, _meta, _arg}, _stack, context) do
# TODO: Function type
{:ok, :dynamic, context}
end
# fun(arg)
def of_expr({fun, _meta, args} = expr, stack, context) when is_atom(fun) and is_list(args) do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
defp for_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
# TODO: Check that of_guard/3 returns a boolean
{:ok, _guard_type, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp for_clause(list, stack, context) when is_list(list) do
reduce_ok(list, context, &for_option(&1, stack, &2))
end
defp for_clause(expr, stack, context) do
of_expr_context(expr, stack, context)
end
defp for_option({:into, expr}, stack, context) do
of_expr_context(expr, stack, context)
end
defp for_option({:reduce, expr}, stack, context) do
of_expr_context(expr, stack, context)
end
defp for_option({:uniq, _}, _stack, context) do
{:ok, context}
end
defp with_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
# TODO: Check that of_guard/3 returns a boolean
{:ok, _guard_type, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp with_clause(list, stack, context) when is_list(list) do
reduce_ok(list, context, &with_option(&1, stack, &2))
end
defp with_clause(expr, stack, context) do
of_expr_context(expr, stack, context)
end
defp with_option({:do, body}, stack, context) do
of_expr_context(body, stack, context)
end
defp with_option({:else, clauses}, stack, context) do
of_clauses(clauses, stack, context)
end
defp of_clauses(clauses, stack, context) do
reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context = acc ->
{patterns, guards} = extract_head(head)
with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context),
{:ok, _expr_type, context} <- of_expr(body, stack, context),
do: {:ok, keep_warnings(acc, context)}
end)
end
defp keep_warnings(context, %{warnings: warnings}) do
%{context | warnings: warnings}
end
defp extract_head([{:when, _meta, args}]) do
case Enum.split(args, -1) do
{patterns, [guards]} -> {patterns, flatten_when(guards)}
{patterns, []} -> {patterns, []}
end
end
defp extract_head(other) do
{other, []}
end
defp flatten_when({:when, _meta, [left, right]}) do
[left | flatten_when(right)]
end
defp flatten_when(other) do
[other]
end
defp of_expr_context(expr, stack, context) do
case of_expr(expr, stack, context) do
{:ok, _type, context} -> {:ok, context}
{:error, reason} -> {:error, reason}
end
end
defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do
{:dynamic, context}
end
defp new_pattern_var(var, context) do
new_var(var, context)
end
end
+15 -31
View File
@@ -1,5 +1,4 @@
defmodule Module.Types.Helpers do
# AST and enumeration helpers.
@moduledoc false
@doc """
@@ -20,10 +19,14 @@ defmodule Module.Types.Helpers do
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
@doc """
Returns the AST metadata.
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
def push_expr_stack(expr, stack) do
%{stack | expr_stack: [expr | stack.expr_stack]}
end
@doc """
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
@@ -38,6 +41,10 @@ defmodule Module.Types.Helpers do
{:ok, acc} ->
do_reduce_ok(tail, acc, fun)
result when elem(result, 0) == :ok ->
result = Tuple.delete_at(result, 0)
do_reduce_ok(tail, result, fun)
{:error, reason} ->
{:error, reason}
end
@@ -74,6 +81,10 @@ defmodule Module.Types.Helpers do
{:ok, elem} ->
do_map_ok(tail, [elem | acc], fun)
result when elem(result, 0) == :ok ->
result = Tuple.delete_at(result, 0)
do_map_ok(tail, [result | acc], fun)
{:error, reason} ->
{:error, reason}
end
@@ -101,23 +112,6 @@ defmodule Module.Types.Helpers do
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
def flat_map_reduce_ok(list, acc, fun) do
do_flat_map_reduce_ok(list, {[], acc}, fun)
end
defp do_flat_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elems, acc} ->
do_flat_map_reduce_ok(tail, {[elems | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_reduce_ok([], {list, acc}, _fun),
do: {:ok, Enum.reverse(Enum.concat(list)), acc}
@doc """
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
@@ -129,14 +123,4 @@ defmodule Module.Types.Helpers do
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
end
end
# TODO: Remove this and let multiple when be treated as multiple clauses,
# meaning they will be intersection types
def guards_to_or([]) do
[]
end
def guards_to_or(guards) do
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
end
end
+356
View File
@@ -0,0 +1,356 @@
defmodule Module.Types.Infer do
@moduledoc false
import Module.Types.Helpers
@doc """
Unifies two types and returns the unified type and an updated typing context
or an error in case of a typing conflict.
"""
def unify(source, target, stack, context) do
case do_unify(source, target, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, reason} ->
if stack.context == :pattern do
case do_unify(target, source, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, _} ->
{:error, reason}
end
else
{:error, reason}
end
end
end
defp do_unify(same, same, _stack, context) do
{:ok, same, context}
end
defp do_unify({:var, var}, type, stack, context) do
unify_var(var, type, stack, context, _var_source = true)
end
defp do_unify(type, {:var, var}, stack, context) do
unify_var(var, type, stack, context, _var_source = false)
end
defp do_unify({:tuple, sources}, {:tuple, targets}, stack, context)
when length(sources) == length(targets) do
result =
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
unify(source, target, stack, context)
end)
case result do
{:ok, types, context} -> {:ok, {:tuple, types}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:list, source}, {:list, target}, stack, context) do
case unify(source, target, stack, context) do
{:ok, type, context} -> {:ok, {:list, type}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
# Since maps in patterns only support literal keys (excluding maps)
# we can do exact type match without subtype checking
unique_right_pairs =
Enum.reject(target_pairs, fn {key, _value} ->
:lists.keyfind(key, 1, source_pairs)
end)
unique_pairs = source_pairs ++ unique_right_pairs
# Build union of all unique key-value pairs between the maps
result =
map_reduce_ok(unique_pairs, context, fn {source_key, source_value}, context ->
case :lists.keyfind(source_key, 1, target_pairs) do
{^source_key, target_value} ->
case unify(source_value, target_value, stack, context) do
{:ok, value, context} -> {:ok, {source_key, value}, context}
{:error, reason} -> {:error, reason}
end
false ->
{:ok, {source_key, source_value}, context}
end
end)
case result do
{:ok, pairs, context} -> {:ok, {:map, pairs}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
defp do_unify(source, target, stack, context) do
if subtype?(source, target, context) do
{:ok, source, context}
else
error({:unable_unify, source, target}, stack, context)
end
end
defp unify_var(var, type, stack, context, var_source?) do
case Map.fetch!(context.types, var) do
:unbound ->
context = refine_var(var, type, stack, context)
if recursive_type?(type, [], context) do
if var_source? do
error({:unable_unify, {:var, var}, type}, stack, context)
else
error({:unable_unify, type, {:var, var}}, stack, context)
end
else
{:ok, {:var, var}, context}
end
var_type ->
context = trace_var(var, type, stack, context)
unify_result =
if var_source? do
unify(var_type, type, stack, context)
else
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, var_type, context} ->
context = refine_var(var, var_type, stack, context)
{:ok, {:var, var}, context}
{:error, reason} ->
{:error, reason}
end
end
end
@doc """
Adds a variable to the typing context and returns its type variables.
If the variable has already been added, return the existing type variable.
"""
def new_var(var, context) do
case Map.fetch(context.vars, var_name(var)) do
{:ok, type} ->
{type, context}
:error ->
type = {:var, context.counter}
vars = Map.put(context.vars, var_name(var), type)
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| vars: vars,
types_to_vars: types_to_vars,
types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
end
@doc """
Set the type for a variable and add trace.
"""
def refine_var(var, type, stack, context) do
types = Map.put(context.types, var, type)
context = %{context | types: types}
trace_var(var, type, stack, context)
end
@doc """
Remove type variable and all its traces.
"""
def remove_var(var, context) do
types = Map.delete(context.types, var)
traces = Map.delete(context.traces, var)
%{context | types: types, traces: traces}
end
defp trace_var(var, type, %{trace: true, expr_stack: expr_stack} = _stack, context) do
line = get_meta(hd(expr_stack))[:line]
trace = {type, expr_stack, {context.file, line}}
traces = Map.update!(context.traces, var, &[trace | &1])
%{context | traces: traces}
end
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
context
end
# Check if a variable is recursive and incompatible with itself
# Bad: `{var} = var`
# Good: `x = y; y = z; z = x`
defp recursive_type?({:var, var} = parent, parents, context) do
case Map.fetch!(context.types, var) do
:unbound ->
false
type ->
if type in parents do
not Enum.all?(parents, &match?({:var, _}, &1))
else
recursive_type?(type, [parent | parents], context)
end
end
end
defp recursive_type?({:list, type} = parent, parents, context) do
recursive_type?(type, [parent | parents], context)
end
defp recursive_type?({:tuple, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:map, pairs} = parent, parents, context) do
Enum.any?(pairs, fn {key, value} ->
recursive_type?(key, [parent | parents], context) or
recursive_type?(value, [parent | parents], context)
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Checks if the first argument is a subtype of the second argument.
Only checks for simple and concrete types.
"""
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
def subtype?(:boolean, :atom, _context), do: true
def subtype?(:float, :number, _context), do: true
def subtype?(:integer, :number, _context), do: true
def subtype?({:tuple, _}, :tuple, _context), do: true
# TODO: Lift unions to unify/3?
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
Enum.all?(left_types, &subtype?(&1, right_union, context))
end
def subtype?(left, {:union, right_types}, context) do
Enum.any?(right_types, &subtype?(left, &1, context))
end
def subtype?(left, right, _context), do: left == right
@doc """
Returns a "simplified" union using `subtype?/3` to remove redundant types.
Due to limitations in `subtype?/3` some overlapping types may still be
included. For example unions with overlapping non-concrete types such as
`{boolean()} | {atom()}` will not be merged or types with variables that
are distinct but equivalent such as `a | b when a ~ b`.
"""
# TODO: Translate union of all top types to dynamic()
def to_union(types, context) when types != [] do
if :dynamic in types do
:dynamic
else
case unique_super_types(flatten_union(types), context) do
[type] -> type
types -> {:union, types}
end
end
end
defp flatten_union(types) do
Enum.flat_map(types, fn
{:union, types} -> flatten_union(types)
type -> [type]
end)
end
# Filter subtypes
# `boolean() | atom()` => `atom()`
# `:foo | atom()` => `atom()`
# Does not unify `true | false` => `boolean()`
defp unique_super_types([type | types], context) do
types = Enum.reject(types, &subtype?(&1, type, context))
if Enum.any?(types, &subtype?(type, &1, context)) do
unique_super_types(types, context)
else
[type | unique_super_types(types, context)]
end
end
defp unique_super_types([], _context) do
[]
end
# Collect relevant information from context and traces to report error
defp error({:unable_unify, left, right}, stack, context) do
{fun, arity} = context.function
line = get_meta(hd(stack.expr_stack))[:line]
location = {context.file, line, {context.module, fun, arity}}
traces = type_traces(context)
common_expr = common_super_expr(traces) || hd(stack.expr_stack)
traces = simplify_traces(traces, context)
{:error, {Module.Types, {:unable_unify, left, right, common_expr, traces}, [location]}}
end
defp type_traces(context) do
Enum.flat_map(context.traces, fn {var_index, traces} ->
expr_var = Map.fetch!(context.types_to_vars, var_index)
Enum.map(traces, &{expr_var, &1})
end)
end
# Only use last expr from trace and tag if trace is for
# a concrete type or type variable
defp simplify_traces(traces, context) do
Enum.flat_map(traces, fn {var, {type, [expr | _], location}} ->
case type do
{:var, var_index} ->
var2 = Map.fetch!(context.types_to_vars, var_index)
[{var, {:var, var2, expr, location}}]
_ ->
[{var, {:type, type, expr, location}}]
end
end)
end
# Find first common super expression among all traces
defp common_super_expr([]) do
nil
end
defp common_super_expr([{_var, {_type, expr_stack, _location}} | traces]) do
Enum.find_value(expr_stack, fn expr ->
common? =
Enum.all?(traces, fn {_var, {_type, expr_stack, _location}} -> expr in expr_stack end)
if common? do
expr
end
end)
end
defp get_meta({_fun, meta, _args}) when is_list(meta), do: meta
defp get_meta(_other), do: []
end
-319
View File
@@ -1,319 +0,0 @@
defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
@prefix quote(do: ...)
@suffix quote(do: ...)
alias Module.ParallelChecker
import Module.Types.Helpers
import Module.Types.Unify
@doc """
Handles open maps (with dynamic => dynamic).
"""
def open_map(args, stack, context, fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
pairs =
for {key, value} <- pairs, not has_unbound_var?(key, context) do
if singleton?(key, context) do
{key, value}
else
{key, to_union([value, :dynamic], context)}
end
end
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
{:ok, {:map, triplets}, context}
end
end
@doc """
Handles closed maps (without dynamic => dynamic).
"""
def closed_map(args, stack, context, fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
{:ok, {:map, closed_to_unions(pairs, context)}, context}
end
end
defp map_pairs(pairs, stack, context, fun) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- fun.(key, stack, context),
{:ok, value_type, context} <- fun.(value, stack, context),
do: {:ok, {key_type, value_type}, context}
end)
end
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
defp closed_to_unions(pairs, context) do
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
{[], pairs} -> pairs_to_unions(pairs, [], context)
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
end
end
defp pairs_to_unions([{key, value} | ahead], behind, context) do
{matched_ahead, values} = find_matching_values(ahead, key, [], [])
# In case nothing matches, use the original ahead
ahead = matched_ahead || ahead
all_values =
[value | values] ++
find_subtype_values(ahead, key, context) ++
find_subtype_values(behind, key, context)
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
end
defp pairs_to_unions([], acc, context) do
acc
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|> Enum.map(fn {key, value} -> {:required, key, value} end)
end
defp find_subtype_values(pairs, key, context) do
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
end
defp find_matching_values([{key, value} | ahead], key, acc, values) do
find_matching_values(ahead, key, acc, [value | values])
end
defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do
find_matching_values(ahead, key, [pair | acc], values)
end
defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values}
defp find_matching_values([], _key, _acc, []), do: {nil, []}
@doc """
Handles structs.
"""
def struct(struct, meta, context) do
context = remote(struct, :__struct__, 0, meta, context)
entries =
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
{:required, {:atom, key}, :dynamic}
end
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
end
## Binary
@doc """
Handles binaries.
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _stack, context, _fun) do
{:ok, context}
end
def binary([head], stack, context, fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
binary_segment(head, head_stack, context, fun)
end
def binary([head | tail], stack, context, fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
case binary_segment(head, head_stack, context, fun) do
{:ok, context} -> binary_many(tail, stack, context, fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_many([last], stack, context, fun) do
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
binary_segment(last, last_stack, context, fun)
end
defp binary_many([head | tail], stack, context, fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
case binary_segment(head, head_stack, context, fun) do
{:ok, context} -> binary_many(tail, stack, context, fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, fun) do
expected_type =
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
float? = collect_binary_specifier(specifiers, &float_type?/1)
# Special case utf and float specifiers because they can be two types as literals
# but only a specific type as a variable in a pattern
cond do
stack.context == :pattern and utf? and is_binary(expr) ->
{:ok, context}
stack.context == :pattern and float? and is_integer(expr) ->
{:ok, context}
true ->
with {:ok, type, context} <- fun.(expr, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
end
defp collect_binary_specifier(other, fun) do
fun.(other)
end
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
defp binary_type(:pattern, {:float, _, _}), do: :float
defp binary_type(_context, {:integer, _, _}), do: :integer
defp binary_type(_context, {:bits, _, _}), do: :binary
defp binary_type(_context, {:bitstring, _, _}), do: :binary
defp binary_type(_context, {:bytes, _, _}), do: :binary
defp binary_type(_context, {:binary, _, _}), do: :binary
defp binary_type(_context, _specifier), do: nil
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
defp utf_type?(_), do: false
defp float_type?({:float, _, _}), do: true
defp float_type?(_), do: false
## Remote
@doc """
Handles remote calls.
"""
def remote(module, fun, arity, meta, context) when is_atom(module) do
# TODO: In the future we may want to warn for modules defined
# in the local context
if Keyword.get(meta, :context_module, false) and context.module != module do
context
else
ParallelChecker.preload_module(context.cache, module)
check_export(module, fun, arity, meta, context)
end
end
def remote(_module, _fun, _arity, _meta, context), do: context
defp check_export(module, fun, arity, meta, context) do
case ParallelChecker.fetch_export(context.cache, module, fun, arity) do
{:ok, :def, reason} ->
check_deprecated(module, fun, arity, reason, meta, context)
{:ok, :defmacro, reason} ->
context = warn(meta, context, {:unrequired_module, module, fun, arity})
check_deprecated(module, fun, arity, reason, meta, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, context) do
warn(meta, context, {:undefined_module, module, fun, arity})
else
context
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, context) do
exports = ParallelChecker.all_exports(context.cache, module)
warn(meta, context, {:undefined_function, module, fun, arity, exports})
else
context
end
end
end
defp check_deprecated(module, fun, arity, reason, meta, context) do
if reason do
warn(meta, context, {:deprecated, module, fun, arity, reason})
else
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, _context), do: false
defp warn_undefined?(_module, :module_info, 0, _context), do: false
defp warn_undefined?(_module, :module_info, 1, _context), do: false
defp warn_undefined?(:erlang, :orelse, 2, _context), do: false
defp warn_undefined?(:erlang, :andalso, 2, _context), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, context) do
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
defp warn(meta, context, warning) do
{fun, arity} = context.function
location = {context.file, meta[:line] || 0, {context.module, fun, arity}}
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
end
## Warning formating
def format_warning({:undefined_module, module, fun, arity}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_warning({:undefined_function, module, fun, arity, exports}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]
end
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_warning({:unrequired_module, module, fun, arity}) do
[
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]
end
end
+162 -130
View File
@@ -1,29 +1,12 @@
defmodule Module.Types.Pattern do
@moduledoc false
alias Module.Types.Of
import Module.Types.{Helpers, Unify}
@doc """
Handles patterns and guards at once.
"""
def of_head(patterns, guards, stack, context) do
with {:ok, types, context} <-
map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)),
# TODO: Check that of_guard/3 returns boolean() | :fail
{:ok, _, context} <- of_guard(guards_to_or(guards), stack, context),
do: {:ok, types, context}
end
import Module.Types.{Helpers, Infer}
@doc """
Return the type and typing context of a pattern expression or an error
in case of a typing conflict.
"""
def of_pattern(pattern, %{context: stack_context} = stack, context)
when stack_context != :pattern do
of_pattern(pattern, %{stack | context: :pattern}, context)
end
# :atom
def of_pattern(atom, _stack, context) when is_atom(atom) do
{:ok, {:atom, atom}, context}
@@ -45,10 +28,10 @@ defmodule Module.Types.Pattern do
end
# <<...>>>
def of_pattern({:<<>>, _meta, args}, stack, context) do
result = Of.binary(args, stack, context, &of_pattern/3)
def of_pattern({:<<>>, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case result do
case reduce_ok(args, context, &of_binary(&1, stack, &2)) do
{:ok, context} -> {:ok, :binary, context}
{:error, reason} -> {:error, reason}
end
@@ -128,11 +111,6 @@ defmodule Module.Types.Pattern do
{:ok, :dynamic, context}
end
# ^var
def of_pattern({:^, _meta, [var]}, _stack, context) do
{:ok, get_var!(var, context), context}
end
# var
def of_pattern(var, _stack, context) when is_var(var) do
{type, context} = new_var(var, context)
@@ -149,7 +127,7 @@ defmodule Module.Types.Pattern do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
{:ok, types, context} -> {:ok, {:tuple, types}, context}
{:error, reason} -> {:error, reason}
end
end
@@ -166,17 +144,10 @@ defmodule Module.Types.Pattern do
# %{...}
def of_pattern({:%{}, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
Of.open_map(args, stack, context, &of_pattern/3)
end
# %Struct{...}
def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
when is_atom(module) do
stack = push_expr_stack(expr, stack)
with {:ok, struct, context} <- Of.struct(module, meta1, context),
{:ok, map, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
unify(map, struct, stack, context)
case of_pairs(args, stack, context) do
{:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context}
{:error, reason} -> {:error, reason}
end
end
@@ -189,80 +160,122 @@ defmodule Module.Types.Pattern do
when is_atom(var_context) do
stack = push_expr_stack(expr, stack)
with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
{:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context}
case of_pairs(args, stack, context) do
{:ok, pairs, context} ->
pairs = [{{:atom, :__struct__}, :atom} | pairs]
{:ok, {:map, pairs}, context}
{:error, reason} ->
{:error, reason}
end
end
# %var{...} and %^var{...}
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) do
# %var{...}
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context)
when is_var(var) do
stack = push_expr_stack(expr, stack)
with {:ok, var_type, context} = of_pattern(var, stack, context),
{:ok, _, context} <- unify(var_type, :atom, stack, context),
{:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
{:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context}
with {:ok, pairs, context} <- of_pairs(args, stack, context),
{var_type, context} = new_var(var, context),
{:ok, _, context} <- unify(var_type, :atom, stack, context) do
pairs = [{{:atom, :__struct__}, var_type} | pairs]
{:ok, {:map, pairs}, context}
end
end
def unify_kinds(:required, _), do: :required
def unify_kinds(_, :required), do: :required
def unify_kinds(:optional, :optional), do: :optional
# %Struct{...}
def of_pattern({:%, _meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
when is_atom(module) do
stack = push_expr_stack(expr, stack)
case of_pairs(args, stack, context) do
{:ok, pairs, context} ->
pairs = [{{:atom, :__struct__}, {:atom, module}} | pairs]
{:ok, {:map, pairs}, context}
{:error, reason} ->
{:error, reason}
end
end
defp of_pairs(pairs, stack, context) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- of_pattern(key, stack, context),
{:ok, value_type, context} <- of_pattern(value, stack, context),
do: {:ok, {key_type, value_type}, context}
end)
end
defp pairs_to_unions(pairs, context) do
# Maps only allow simple literal keys in patterns so
# we do not have to do subtype checking
Enum.reduce(pairs, [], fn {key, value}, pairs ->
case :lists.keyfind(key, 1, pairs) do
{^key, {:union, union}} ->
:lists.keystore(key, 1, pairs, {key, to_union([value | union], context)})
{^key, original_value} ->
:lists.keystore(key, 1, pairs, {key, to_union([value, original_value], context)})
false ->
[{key, value} | pairs]
end
end)
end
## GUARDS
# TODO: Some guards can be changed to intersection types or higher order types
@boolean {:union, [{:atom, true}, {:atom, false}]}
@number {:union, [:integer, :float]}
@guard_functions %{
{:is_atom, 1} => {[:atom], @boolean},
{:is_binary, 1} => {[:binary], @boolean},
{:is_bitstring, 1} => {[:binary], @boolean},
{:is_boolean, 1} => {[@boolean], @boolean},
{:is_float, 1} => {[:float], @boolean},
{:is_function, 1} => {[:fun], @boolean},
{:is_function, 2} => {[:fun, :integer], @boolean},
{:is_integer, 1} => {[:integer], @boolean},
{:is_list, 1} => {[{:list, :dynamic}], @boolean},
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean},
{:is_map_key, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
{:is_number, 1} => {[@number], @boolean},
{:is_pid, 1} => {[:pid], @boolean},
{:is_port, 1} => {[:port], @boolean},
{:is_reference, 1} => {[:reference], @boolean},
{:is_tuple, 1} => {[:tuple], @boolean},
{:<, 2} => {[:dynamic, :dynamic], @boolean},
{:"=<", 2} => {[:dynamic, :dynamic], @boolean},
{:>, 2} => {[:dynamic, :dynamic], @boolean},
{:>=, 2} => {[:dynamic, :dynamic], @boolean},
{:"/=", 2} => {[:dynamic, :dynamic], @boolean},
{:"=/=", 2} => {[:dynamic, :dynamic], @boolean},
{:==, 2} => {[:dynamic, :dynamic], @boolean},
{:"=:=", 2} => {[:dynamic, :dynamic], @boolean},
{:*, 2} => {[@number, @number], @number},
{:+, 1} => {[@number], @number},
{:+, 2} => {[@number, @number], @number},
{:-, 1} => {[@number], @number},
{:-, 2} => {[@number, @number], @number},
{:/, 2} => {[@number, @number], @number},
{:abs, 1} => {[@number], @number},
{:ceil, 1} => {[@number], :integer},
{:floor, 1} => {[@number], :integer},
{:round, 1} => {[@number], :integer},
{:trunc, 1} => {[@number], :integer},
{:is_atom, 1} => {[:atom], :boolean},
{:is_binary, 1} => {[:binary], :boolean},
{:is_bitstring, 1} => {[:binary], :boolean},
{:is_boolean, 1} => {[:boolean], :boolean},
{:is_float, 1} => {[:float], :boolean},
{:is_function, 1} => {[:fun], :boolean},
{:is_function, 2} => {[:fun, :integer], :boolean},
{:is_integer, 1} => {[:integer], :boolean},
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
{:is_map, 1} => {[{:map, []}], :boolean},
{:is_map_key, 2} => {[:dynamic, {:map, []}], :dynamic},
{:is_number, 1} => {[:number], :boolean},
{:is_pid, 1} => {[:pid], :boolean},
{:is_port, 1} => {[:port], :boolean},
{:is_reference, 1} => {[:reference], :boolean},
{:is_tuple, 1} => {[:tuple], :boolean},
{:<, 2} => {[:dynamic, :dynamic], :boolean},
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
{:>, 2} => {[:dynamic, :dynamic], :boolean},
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
{:==, 2} => {[:dynamic, :dynamic], :boolean},
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
{:*, 2} => {[:number, :number], :number},
{:+, 1} => {[:number], :number},
{:+, 2} => {[:number, :number], :number},
{:-, 1} => {[:number], :number},
{:-, 2} => {[:number, :number], :number},
{:/, 2} => {[:number, :number], :number},
{:abs, 1} => {[:number], :number},
{:ceil, 1} => {[:number], :integer},
{:floor, 1} => {[:number], :integer},
{:round, 1} => {[:number], :integer},
{:trunc, 1} => {[:number], :integer},
{:element, 2} => {[:integer, :tuple], :dynamic},
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
{:length, 1} => {[{:list, :dynamic}], :integer},
{:map_get, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
{:map_size, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], :integer},
{:map_get, 2} => {[:dynamic, {:map, []}], :dynamic},
{:map_size, 1} => {[{:map, []}], :integer},
{:tl, 1} => {[{:list, :dynamic}], :dynamic},
{:tuple_size, 1} => {[:tuple], :integer},
{:node, 1} => {[{:union, [:pid, :reference, :port]}], :atom},
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
{:bit_size, 1} => {[:binary], :integer},
{:byte_size, 1} => {[:binary], :integer},
{:size, 1} => {[{:union, [:binary, :tuple]}], @boolean},
{:size, 1} => {[{:union, [:binary, :tuple]}], :boolean},
{:div, 2} => {[:integer, :integer], :integer},
{:rem, 2} => {[:integer, :integer], :integer},
{:node, 0} => {[], :atom},
@@ -273,15 +286,13 @@ defmodule Module.Types.Pattern do
{:bxor, 2} => {[:integer, :integer], :integer},
{:bsl, 2} => {[:integer, :integer], :integer},
{:bsr, 2} => {[:integer, :integer], :integer},
{:or, 2} => {[@boolean, @boolean], @boolean},
{:and, 2} => {[@boolean, @boolean], @boolean},
{:xor, 2} => {[@boolean, @boolean], @boolean},
{:not, 1} => {[@boolean], @boolean}
{:xor, 2} => {[:boolean, :boolean], :boolean},
{:not, 1} => {[:boolean], :boolean}
# Following guards are matched explicitly to handle
# type guard functions such as is_atom/1
# {:andalso, 2} => {[@boolean, @boolean], @boolean}
# {:orelse, 2} => {[@boolean, @boolean], @boolean}
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
}
@type_guards [
@@ -306,10 +317,6 @@ defmodule Module.Types.Pattern do
Refines the type variables in the typing context using type check guards
such as `is_integer/1`.
"""
def of_guard(expr, %{context: stack_context} = stack, context) when stack_context != :pattern do
of_guard(expr, %{stack | context: :pattern}, context)
end
def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
fresh_context = fresh_context(context)
@@ -317,9 +324,9 @@ defmodule Module.Types.Pattern do
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
{:ok, _, context} <- unify(right_type, @boolean, stack, context),
do: {:ok, @boolean, context}
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
do: {:ok, :boolean, context}
end
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
@@ -329,25 +336,10 @@ defmodule Module.Types.Pattern do
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
do: {:ok, @boolean, context}
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
do: {:ok, :boolean, context}
end
# The unary operators + and - are special cased to avoid common warnings until
# we add support for intersection types for the guard functions
# -integer / +integer
def of_guard({{:., _, [:erlang, guard]}, _, [integer]}, _stack, context)
when guard in [:+, :-] and is_integer(integer) do
{:ok, :integer, context}
end
# -float / +float
def of_guard({{:., _, [:erlang, guard]}, _, [float]}, _stack, context)
when guard in [:+, :-] and is_float(float) do
{:ok, :float, context}
end
# fun(args)
def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{param_types, return_type} = guard_signature(guard, length(args))
@@ -361,7 +353,7 @@ defmodule Module.Types.Pattern do
with {:ok, arg_types, context} <-
map_reduce_ok(args, context, &of_guard(&1, arg_stack, &2)),
{:ok, context} <- unify_call(arg_types, param_types, stack, context) do
{:ok, context} <- unify_call(param_types, arg_types, stack, context) do
{arg_types, guard_sources} =
case arg_types do
[{:var, index} | rest_arg_types] when type_guard? ->
@@ -390,18 +382,11 @@ defmodule Module.Types.Pattern do
end
end
# map.field
def of_guard({{:., meta1, [map, field]}, meta2, []}, stack, context) do
of_guard({{:., meta1, [:erlang, :map_get]}, meta2, [field, map]}, stack, context)
end
# var
def of_guard(var, _stack, context) when is_var(var) do
type = Map.fetch!(context.vars, var_name(var))
{:ok, type, context}
end
# other literals
def of_guard(expr, stack, context) do
# Fall back to of_pattern/3 for literals
of_pattern(expr, stack, context)
@@ -413,9 +398,9 @@ defmodule Module.Types.Pattern do
%{context | types: types, traces: traces}
end
defp unify_call(args, params, stack, context) do
reduce_ok(Enum.zip(args, params), context, fn {arg, param}, context ->
case unify(arg, param, stack, context) do
defp unify_call(params, args, stack, context) do
reduce_ok(Enum.zip(params, args), context, fn {param, arg}, context ->
case unify(param, arg, stack, context) do
{:ok, _, context} -> {:ok, context}
{:error, reason} -> {:error, reason}
end
@@ -460,6 +445,7 @@ defmodule Module.Types.Pattern do
cond do
:fail in sources -> [:fail]
:guarded_fail in sources -> [:guarded_fail]
:guarded in sources -> [:guarded]
true -> []
end
@@ -470,7 +456,7 @@ defmodule Module.Types.Pattern do
# When the failing guard function wont fail due to type check function before it,
# for example: is_list(x) and length(x)
if :guarded in left and :fail in right do
[:guarded]
[:guarded_fail]
else
join_guard_source(left, right)
end
@@ -543,6 +529,52 @@ defmodule Module.Types.Pattern do
{:ok, context}
end
# binary-pattern :: specifier
defp of_binary({:"::", _meta, [expr, specifiers]} = full_expr, stack, context) do
{expected_type, utf?} = collect_binary_type(specifiers) || {:integer, false}
stack = push_expr_stack(full_expr, stack)
# Special case utf specifiers with binary literals since they allow
# both integer and binary literals but variables are always integer
if is_binary(expr) and utf? do
{:ok, context}
else
with {:ok, type, context} <- of_pattern(expr, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# binary-pattern
defp of_binary(expr, stack, context) do
case of_pattern(expr, stack, context) do
{:ok, type, context} when type in [:integer, :float, :binary] ->
{:ok, context}
{:ok, type, _context} ->
{:error, {:invalid_binary_type, type}}
{:error, reason} ->
{:error, reason}
end
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_type({:-, _meta, [left, right]}),
do: collect_binary_type(left) || collect_binary_type(right)
defp collect_binary_type({:integer, _, _}), do: {:integer, false}
defp collect_binary_type({:float, _, _}), do: {:float, false}
defp collect_binary_type({:bits, _, _}), do: {:binary, false}
defp collect_binary_type({:bitstring, _, _}), do: {:binary, false}
defp collect_binary_type({:bytes, _, _}), do: {:binary, false}
defp collect_binary_type({:binary, _, _}), do: {:binary, false}
defp collect_binary_type({:utf8, _, _}), do: {:integer, true}
defp collect_binary_type({:utf16, _, _}), do: {:integer, true}
defp collect_binary_type({:utf32, _, _}), do: {:integer, true}
defp collect_binary_type(_), do: nil
defp guard_signature(name, arity) do
Map.fetch!(@guard_functions, {name, arity})
end
-700
View File
@@ -1,700 +0,0 @@
defmodule Module.Types.Unify do
@moduledoc false
import Module.Types.Helpers
# Those are the simple types known to the system:
#
# :dynamic
# {:var, var}
# {:atom, atom} < :atom
# :integer
# :float
# :pid
# :port
# :reference
#
# Those are the composite types:
#
# {:list, type}
# {:tuple, size, [type]} < :tuple
# {:union, [type]}
# {:map, [{:required | :optional, key_type, value_type}]}
#
# Once new types are added, they should be considered in:
#
# * unify (all)
# * format_type (all)
# * subtype? (subtypes only)
# * has_unbound_var? (composite only)
# * recursive_type? (composite only)
#
@doc """
Unifies two types and returns the unified type and an updated typing context
or an error in case of a typing conflict.
"""
def unify(source, target, stack, context) do
case do_unify(source, target, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, reason} ->
if stack.context == :pattern do
case do_unify(target, source, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, _} ->
{:error, reason}
end
else
{:error, reason}
end
end
end
defp do_unify(same, same, _stack, context) do
{:ok, same, context}
end
defp do_unify(type, {:var, var}, stack, context) do
case context.types do
%{^var => {:var, var_type}} ->
do_unify(type, {:var, var_type}, stack, context)
%{} ->
unify_var(var, type, stack, context, _var_source = false)
end
end
defp do_unify({:var, var}, type, stack, context) do
case context.types do
%{^var => {:var, var_type}} ->
do_unify({:var, var_type}, type, stack, context)
%{} ->
unify_var(var, type, stack, context, _var_source = true)
end
end
defp do_unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do
result =
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
unify(source, target, stack, context)
end)
case result do
{:ok, types, context} -> {:ok, {:tuple, n, types}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:list, source}, {:list, target}, stack, context) do
case unify(source, target, stack, context) do
{:ok, type, context} -> {:ok, {:list, type}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
unify_maps(source_pairs, target_pairs, stack, context)
end
defp do_unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
defp do_unify(:dynamic, target, _stack, context) do
{:ok, target, context}
end
defp do_unify(source, target, stack, context) do
cond do
# This condition exists to handle unions with unbound vars.
# TODO: handle unions properly. Note we can easily unify
# "union < type" even if union has vars as the vars must be
# type
(match?({:union, _}, source) and has_unbound_var?(source, context)) or
(match?({:union, _}, target) and has_unbound_var?(target, context)) ->
{:ok, source, context}
subtype?(source, target, context) ->
{:ok, source, context}
true ->
error(:unable_unify, {source, target, stack}, context)
end
end
defp unify_var(var, :dynamic, _stack, context, _var_source?) do
{:ok, {:var, var}, context}
end
defp unify_var(var, type, stack, context, var_source?) do
case context.types do
%{^var => :unbound} ->
context = refine_var(var, type, stack, context)
stack = push_unify_stack(var, stack)
if recursive_type?(type, [], context) do
if var_source? do
error(:unable_unify, {{:var, var}, type, stack}, context)
else
error(:unable_unify, {type, {:var, var}, stack}, context)
end
else
{:ok, {:var, var}, context}
end
%{^var => var_type} ->
# Only add trace if the variable wasn't already "expanded"
context =
if variable_expanded?(var, stack, context) do
context
else
trace_var(var, type, stack, context)
end
stack = push_unify_stack(var, stack)
unify_result =
if var_source? do
unify(var_type, type, stack, context)
else
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, var_type, context} ->
context = refine_var(var, var_type, stack, context)
{:ok, {:var, var}, context}
{:error, reason} ->
{:error, reason}
end
end
end
# * All required keys on each side need to match to the other side.
# * All optional keys on each side that do not match must be discarded.
defp unify_maps(source_pairs, target_pairs, stack, context) do
{source_required, source_optional} = split_pairs(source_pairs)
{target_required, target_optional} = split_pairs(target_pairs)
with {:ok, source_required_pairs, context} <-
unify_source_required(source_required, target_pairs, stack, context),
{:ok, target_required_pairs, context} <-
unify_target_required(target_required, source_pairs, stack, context),
{:ok, source_optional_pairs, context} <-
unify_source_optional(source_optional, target_optional, stack, context),
{:ok, target_optional_pairs, context} <-
unify_target_optional(target_optional, source_optional, stack, context) do
# Remove duplicate pairs from matching in both left and right directions
pairs =
Enum.uniq(
source_required_pairs ++
target_required_pairs ++
source_optional_pairs ++
target_optional_pairs
)
{:ok, {:map, pairs}, context}
else
{:error, :unify} ->
error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context)
{:error, context} ->
{:error, context}
end
end
defp unify_source_required(source_required, target_pairs, stack, context) do
map_reduce_ok(source_required, context, fn {source_key, source_value}, context ->
Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{:required, source_key, source_value}]}
target_map = {:map, [{target_kind, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
defp unify_target_required(target_required, source_pairs, stack, context) do
map_reduce_ok(target_required, context, fn {target_key, target_value}, context ->
Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{source_kind, source_key, source_value}]}
target_map = {:map, [{:required, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
defp unify_source_optional(source_optional, target_optional, stack, context) do
flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context ->
Enum.find_value(target_optional, fn {target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
defp unify_target_optional(target_optional, source_optional, stack, context) do
flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context ->
Enum.find_value(source_optional, fn {source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
defp split_pairs(pairs) do
{required, optional} =
Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end)
required = Enum.map(required, fn {_kind, key, value} -> {key, value} end)
optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end)
{required, optional}
end
defp error(type, reason, context), do: {:error, {type, reason, context}}
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def push_expr_stack(expr, stack) do
%{stack | last_expr: expr}
end
@doc """
Gets a variable.
"""
def get_var!(var, context) do
Map.fetch!(context.vars, var_name(var))
end
@doc """
Adds a variable to the typing context and returns its type variable.
If the variable has already been added, return the existing type variable.
"""
def new_var(var, context) do
var_name = var_name(var)
case context.vars do
%{^var_name => type} ->
{type, context}
%{} ->
type = {:var, context.counter}
vars = Map.put(context.vars, var_name, type)
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| vars: vars,
types_to_vars: types_to_vars,
types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
end
@doc """
Adds an internal variable to the typing context and returns its type variable.
An internal variable is used to help unify complex expressions,
it does not belong to a specific AST expression.
"""
def add_var(context) do
type = {:var, context.counter}
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
@doc """
Resolves a variable raising if it is unbound.
"""
def resolve_var({:var, var}, context) do
case context.types do
%{^var => :unbound} -> raise "cannot resolve unbound var"
%{^var => type} -> resolve_var(type, context)
end
end
def resolve_var(other, _context), do: other
# Check unify stack to see if variable was already expanded
defp variable_expanded?(var, stack, context) do
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))
end
defp variable_same?(left, right, context) do
case context.types do
%{^left => {:var, new_left}} ->
variable_same?(new_left, right, context)
%{^right => {:var, new_right}} ->
variable_same?(left, new_right, context)
%{} ->
false
end
end
defp push_unify_stack(var, stack) do
%{stack | unify_stack: [var | stack.unify_stack]}
end
@doc """
Set the type for a variable and add trace.
"""
def refine_var(var, type, stack, context) do
types = Map.put(context.types, var, type)
context = %{context | types: types}
trace_var(var, type, stack, context)
end
@doc """
Remove type variable and all its traces.
"""
def remove_var(var, context) do
types = Map.delete(context.types, var)
traces = Map.delete(context.traces, var)
%{context | types: types, traces: traces}
end
defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do
line = get_meta(last_expr)[:line]
trace = {type, last_expr, {context.file, line}}
traces = Map.update!(context.traces, var, &[trace | &1])
%{context | traces: traces}
end
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
context
end
# Check if a variable is recursive and incompatible with itself
# Bad: `{var} = var`
# Good: `x = y; y = z; z = x`
defp recursive_type?({:var, var} = parent, parents, context) do
case context.types do
%{^var => :unbound} ->
false
%{^var => type} ->
if type in parents do
not Enum.all?(parents, &match?({:var, _}, &1))
else
recursive_type?(type, [parent | parents], context)
end
end
end
defp recursive_type?({:list, type} = parent, parents, context) do
recursive_type?(type, [parent | parents], context)
end
defp recursive_type?({:union, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:tuple, _, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:map, pairs} = parent, parents, context) do
Enum.any?(pairs, fn {_kind, key, value} ->
recursive_type?(key, [parent | parents], context) or
recursive_type?(value, [parent | parents], context)
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Checks if the type has a type var.
"""
def has_unbound_var?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> true
%{^var => type} -> has_unbound_var?(type, context)
end
end
def has_unbound_var?({:tuple, _, args}, context),
do: Enum.any?(args, &has_unbound_var?(&1, context))
def has_unbound_var?({:union, args}, context),
do: Enum.any?(args, &has_unbound_var?(&1, context))
def has_unbound_var?({:list, arg}, context),
do: has_unbound_var?(arg, context)
def has_unbound_var?({:map, pairs}, context) do
Enum.any?(pairs, fn {_, key, value} ->
has_unbound_var?(key, context) or has_unbound_var?(value, context)
end)
end
def has_unbound_var?(_type, _context), do: false
@doc """
Returns true if it is a singleton type.
Only atoms are singleton types. Unbound vars are not
considered singleton types.
"""
def singleton?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> singleton?(type, context)
end
end
def singleton?({:atom, _}, _context), do: true
def singleton?(_type, _context), do: false
@doc """
Checks if the first argument is a subtype of the second argument.
This function assumes that:
* unbound variables are not subtype of anything
* dynamic is not considered a subtype of all other types but the top type.
This allows this function can be used for ordering, in other cases, you
may need to check for both sides
"""
def subtype?(type, type, _context), do: true
def subtype?({:var, var}, other, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(type, other, context)
end
end
def subtype?(other, {:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(other, type, context)
end
end
def subtype?(_, :dynamic, _context), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
# Composite
def subtype?({:tuple, _, _}, :tuple, _context), do: true
def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do
left_types
|> Enum.zip(right_types)
|> Enum.any?(fn {left, right} -> subtype?(left, right, context) end)
end
def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do
Enum.all?(left_pairs, fn
{:required, left_key, left_value} ->
Enum.any?(right_pairs, fn {_, right_key, right_value} ->
subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context)
end)
{:optional, _, _} ->
true
end)
end
def subtype?({:list, left}, {:list, right}, context) do
subtype?(left, right, context)
end
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
Enum.all?(left_types, &subtype?(&1, right_union, context))
end
def subtype?(left, {:union, right_types}, context) do
Enum.any?(right_types, &subtype?(left, &1, context))
end
def subtype?({:union, left_types}, right, context) do
Enum.all?(left_types, &subtype?(&1, right, context))
end
def subtype?(_left, _right, _context), do: false
@doc """
Returns a "simplified" union using `subtype?/3` to remove redundant types.
Due to limitations in `subtype?/3` some overlapping types may still be
included. For example unions with overlapping non-concrete types such as
`{boolean()} | {atom()}` will not be merged or types with variables that
are distinct but equivalent such as `a | b when a ~ b`.
"""
def to_union([type], _context), do: type
def to_union(types, context) when types != [] do
flat_types = flatten_union(types)
case unique_super_types(flat_types, context) do
[type] -> type
types -> {:union, types}
end
end
defp flatten_union(types) do
Enum.flat_map(types, fn
{:union, types} -> flatten_union(types)
type -> [type]
end)
end
# Filter subtypes
#
# `boolean() | atom()` => `atom()`
# `:foo | atom()` => `atom()`
#
# Does not merge `true | false` => `boolean()`
defp unique_super_types([type | types], context) do
types = Enum.reject(types, &subtype?(&1, type, context))
if Enum.any?(types, &subtype?(type, &1, context)) do
unique_super_types(types, context)
else
[type | unique_super_types(types, context)]
end
end
defp unique_super_types([], _context) do
[]
end
@doc """
Formats types.
The second argument says when complex types such as maps and
structs should be simplified and not shown.
"""
def format_type({:map, pairs}, true) do
case List.keyfind(pairs, {:atom, :__struct__}, 1) do
{:required, {:atom, :__struct__}, {:atom, struct}} ->
"%#{inspect(struct)}{}"
_ ->
"map()"
end
end
def format_type({:union, types}, simplify?) do
"#{Enum.map_join(types, " | ", &format_type(&1, simplify?))}"
end
def format_type({:tuple, _, types}, simplify?) do
"{#{Enum.map_join(types, ", ", &format_type(&1, simplify?))}}"
end
def format_type({:list, type}, simplify?) do
"[#{format_type(type, simplify?)}]"
end
def format_type({:map, pairs}, false) do
case List.keytake(pairs, {:atom, :__struct__}, 1) do
{{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} ->
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
_ ->
"%{#{format_map_pairs(pairs)}}"
end
end
def format_type({:atom, literal}, _simplify?) do
inspect(literal)
end
def format_type({:var, index}, _simplify?) do
"var#{index}"
end
def format_type(atom, _simplify?) when is_atom(atom) do
"#{atom}()"
end
defp format_map_pairs(pairs) do
{atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1))
{required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1))
Enum.map_join(atoms ++ required ++ optional, ", ", fn
{:required, {:atom, atom}, right} ->
"#{atom}: #{format_type(right, false)}"
{:required, left, right} ->
"#{format_type(left, false)} => #{format_type(right, false)}"
{:optional, left, right} ->
"optional(#{format_type(left, false)}) => #{format_type(right, false)}"
end)
end
end
+3 -3
View File
@@ -106,7 +106,7 @@ defmodule Node do
For more information, see `:erlang.monitor_node/2`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
"""
@spec monitor(t, boolean) :: true
def monitor(node, flag) do
@@ -119,7 +119,7 @@ defmodule Node do
For more information, see `:erlang.monitor_node/3`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
"""
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
def monitor(node, flag, options) do
@@ -219,7 +219,7 @@ defmodule Node do
If `node` does not exist, a useless PID is returned.
For the list of available options, see `:erlang.spawn/4`.
For the list of available options, see `:erlang.spawn/5`.
Inlined by the compiler.
"""
+5 -2
View File
@@ -152,8 +152,8 @@ defmodule Path do
## Examples
Path.expand("/foo/bar/../baz")
#=> "/foo/baz"
Path.expand("/foo/bar/../bar")
#=> "/foo/bar"
"""
@spec expand(t) :: binary
@@ -565,6 +565,9 @@ defmodule Path do
"""
@spec split(t) :: [binary]
# Work around a bug in Erlang on Unix-like operating systems
def split(""), do: []
def split(path) do
:filename.split(IO.chardata_to_string(path))
end
+2 -2
View File
@@ -29,7 +29,7 @@ defmodule Port do
After sending those two messages, we invoked the IEx helper `flush()`,
which printed all messages received from the port, in this case we got
"hello" and "world" back. Note that the messages are in binary because we
"hello" and "world" back. Notice the messages are in binary because we
passed the `:binary` option when opening the port in `Port.open/2`. Without
such option, it would have yielded a list of bytes.
@@ -180,7 +180,7 @@ defmodule Port do
@type name ::
{:spawn, charlist | binary}
| {:spawn_driver, charlist | binary}
| {:spawn_executable, :file.name_all()}
| {:spawn_executable, charlist | atom}
| {:fd, non_neg_integer, non_neg_integer}
@doc """
+1 -2
View File
@@ -705,8 +705,7 @@ defmodule Process do
"""
@spec flag(:error_handler, module) :: module
@spec flag(:max_heap_size, heap_size) :: heap_size
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
@spec flag(:message_queue_data, :erlang.message_queue_data()) :: :erlang.message_queue_data()
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:priority, priority_level) :: priority_level
+1 -5
View File
@@ -43,14 +43,10 @@ defmodule Protocol do
def size(tuple), do: tuple_size(tuple)
end
Note that we didn't implement it for lists as we don't have the
Notice we didn't implement it for lists as we don't have the
`size` information on lists, rather its value needs to be
computed with `length`.
The data structure you are implementing the protocol for
must be the first argument to all functions defined in the
protocol.
It is possible to implement protocols for all Elixir types:
* Structs (see below)
+27 -32
View File
@@ -401,7 +401,6 @@ defmodule Record do
# Using {} here is safe, since it's not valid AST
default = if Macro.Env.in_match?(caller), do: {:_, [], nil}, else: {}
{default, keyword} = Keyword.pop(keyword, :_, default)
{keyword, exprs} = hoist_expressions(keyword, caller)
{elements, remaining} =
Enum.map_reduce(fields, keyword, fn {key, field_default}, remaining ->
@@ -414,7 +413,6 @@ defmodule Record do
case remaining do
[] ->
quote(do: {unquote(tag), unquote_splicing(elements)})
|> maybe_prepend_reversed_exprs(exprs)
[{key, _} | _] ->
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
@@ -427,45 +425,27 @@ defmodule Record do
raise ArgumentError, "cannot invoke update style macro inside match"
end
{keyword, exprs} = hoist_expressions(keyword, caller)
if Keyword.has_key?(keyword, :_) do
message = "updating a record with a default (:_) is equivalent to creating a new record"
IO.warn(message, Macro.Env.stacktrace(caller))
create(tag, fields, keyword, caller)
else
updates =
Enum.map(keyword, fn {key, value} ->
if index = find_index(fields, key, 2) do
{index, value}
else
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
end
end)
case build_updates(keyword, fields, [], [], []) do
{updates, [], []} ->
build_update(updates, var)
build_update(updates, var) |> maybe_prepend_reversed_exprs(exprs)
{updates, vars, exprs} ->
quote do
{unquote_splicing(:lists.reverse(vars))} = {unquote_splicing(:lists.reverse(exprs))}
unquote(build_update(updates, var))
end
{:error, key} ->
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
end
end
end
defp hoist_expressions(keyword, %{context: nil}) do
Enum.map_reduce(keyword, [], fn {key, expr}, acc ->
if simple_argument?(expr) do
{{key, expr}, acc}
else
var = Macro.var(key, __MODULE__)
{{key, var}, [{:=, [], [var, expr]} | acc]}
end
end)
end
defp hoist_expressions(keyword, _), do: {keyword, []}
defp maybe_prepend_reversed_exprs(expr, []),
do: expr
defp maybe_prepend_reversed_exprs(expr, exprs),
do: {:__block__, [], :lists.reverse([expr | exprs])}
defp build_update(updates, initial) do
updates
|> Enum.sort(fn {left, _}, {right, _} -> right <= left end)
@@ -474,6 +454,21 @@ defmodule Record do
end)
end
defp build_updates([{key, value} | rest], fields, updates, vars, exprs) do
if index = find_index(fields, key, 2) do
if simple_argument?(value) do
build_updates(rest, fields, [{index, value} | updates], vars, exprs)
else
var = Macro.var(key, __MODULE__)
build_updates(rest, fields, [{index, var} | updates], [var | vars], [value | exprs])
end
else
{:error, key}
end
end
defp build_updates([], _fields, updates, vars, exprs), do: {updates, vars, exprs}
defp simple_argument?({name, _, ctx}) when is_atom(name) and is_atom(ctx), do: true
defp simple_argument?(other), do: Macro.quoted_literal?(other)
+20 -18
View File
@@ -16,14 +16,13 @@ defmodule Regex do
~r/foo/iu
Regular expressions created via sigils are pre-compiled and stored
in the `.beam` file. Note that this may be a problem if you are precompiling
in the `.beam` file. Notice this may be a problem if you are precompiling
Elixir, see the "Precompilation" section for more information.
A Regex is represented internally as the `Regex` struct. Therefore,
`%Regex{}` can be used whenever there is a need to match on them.
Keep in mind that all of the structs fields are private. There is
also not guarantee two regular expressions from the same source are
equal, for example:
Keep in mind it is not guaranteed two regular expressions from the
same source are equal, for example:
~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/
@@ -38,10 +37,9 @@ defmodule Regex do
The modifiers available when creating a Regex are:
* `unicode` (u) - enables Unicode specific patterns like `\p` and causes
character classes like `\w`, `\W`, `\s`, etc. to also match on Unicode
(see examples below in "Character classes"). It expects valid Unicode
strings to be given on match
* `unicode` (u) - enables Unicode specific patterns like `\p` and change
modifiers like `\w`, `\W`, `\s` and friends to also match on Unicode.
It expects valid Unicode strings to be given on match
* `caseless` (i) - adds case insensitivity
@@ -127,10 +125,6 @@ defmodule Regex do
false
iex> String.match?("josé", ~r/^[[:lower:]]+$/u)
true
iex> Regex.replace(~r/\s/, "Unicode\u00A0spaces", "-")
"Unicode spaces"
iex> Regex.replace(~r/\s/u, "Unicode\u00A0spaces", "-")
"Unicode-spaces"
## Precompilation
@@ -224,7 +218,7 @@ defmodule Regex do
and recompiles the regex in case of version mismatch.
"""
@doc since: "1.4.0"
@spec recompile(t) :: {:ok, t} | {:error, any}
@spec recompile(t) :: t
def recompile(%Regex{} = regex) do
version = version()
@@ -279,9 +273,17 @@ defmodule Regex do
@doc """
Returns `true` if the given `term` is a regex.
Otherwise returns `false`.
## Examples
iex> Regex.regex?(~r/foo/)
true
iex> Regex.regex?(0)
false
"""
# TODO: deprecate permanently on Elixir v1.15
@doc deprecated: "Use Kernel.is_struct/2 or pattern match on %Regex{} instead"
@spec regex?(any) :: boolean
def regex?(term)
def regex?(%Regex{}), do: true
def regex?(_), do: false
@@ -757,12 +759,12 @@ defmodule Regex do
end
end
defp get_index(_string, {pos, _length}) when pos < 0 do
defp get_index(_string, {pos, _len}) when pos < 0 do
""
end
defp get_index(string, {pos, length}) do
<<_::size(pos)-binary, res::size(length)-binary, _::binary>> = string
defp get_index(string, {pos, len}) do
<<_::size(pos)-binary, res::size(len)-binary, _::binary>> = string
res
end
+7 -35
View File
@@ -3,7 +3,7 @@ defmodule Registry do
A local, decentralized and scalable key-value process storage.
It allows developers to lookup one or more processes with a given key.
If the registry has `:unique` keys, a key points to 0 or 1 process.
If the registry has `:unique` keys, a key points to 0 or 1 processes.
If the registry allows `:duplicate` keys, a single key may point to any
number of processes. In both cases, different keys could identify the
same process.
@@ -261,8 +261,9 @@ defmodule Registry do
end
@doc false
def unregister_name({registry, key}), do: unregister(registry, key)
def unregister_name({registry, key, _value}), do: unregister(registry, key)
def unregister_name({registry, key}) do
unregister(registry, key)
end
## Registry API
@@ -783,8 +784,7 @@ defmodule Registry do
end
@doc """
Unregisters entries for keys matching a pattern associated to the current
process in `registry`.
Unregister entries for a given key matching a pattern.
## Examples
@@ -922,7 +922,7 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key_partition)
{pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition)
# Note that we write first to the pid_ets table because it will
# Notice we write first to the pid_ets table because it will
# always be able to do the cleanup. If we register first to the
# key one and the process crashes, the key will stay there forever.
Process.link(pid_server)
@@ -958,7 +958,7 @@ defmodule Registry do
if :ets.insert_new(key_ets, entry) do
:ok
else
# Note that we have to call register_key recursively
# Notice we have to call register_key recursively
# because we are always at odds of a race condition.
case :ets.lookup(key_ets, key) do
[{^key, {pid, _}} = current] ->
@@ -1033,34 +1033,6 @@ defmodule Registry do
end
end
@doc """
Deletes registry metadata for the given `key` in `registry`.
## Examples
iex> Registry.start_link(keys: :unique, name: Registry.DeleteMetaTest)
iex> Registry.put_meta(Registry.DeleteMetaTest, :custom_key, "custom_value")
:ok
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.delete_meta(Registry.DeleteMetaTest, :custom_key)
:ok
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
:error
"""
@doc since: "1.11.0"
@spec delete_meta(registry, meta_key) :: :ok
def delete_meta(registry, key) when is_atom(registry) and (is_atom(key) or is_tuple(key)) do
try do
:ets.delete(registry, key)
:ok
catch
:error, :badarg ->
raise ArgumentError, "unknown registry: #{inspect(registry)}"
end
end
@doc """
Returns the number of registered keys in a registry.
It runs in constant time.
+3 -3
View File
@@ -21,7 +21,7 @@ defmodule Stream do
iex> Enum.map(stream, &(&1 + 1))
[3, 5, 7]
Note that we started with a range and then we created a stream that is
Notice we started with a range and then we created a stream that is
meant to multiply each element in the range by 2. At this point, no
computation was done. Only when `Enum.map/2` is called we actually
enumerate over each element in the range, multiplying it by 2 and adding 1.
@@ -46,7 +46,7 @@ defmodule Stream do
6
#=> [2, 4, 6]
Note that we first printed each element in the list, then multiplied each
Notice that we first printed each element in the list, then multiplied each
element by 2 and finally printed each new value. In this example, the list
was enumerated three times. Let's see an example with streams:
@@ -68,7 +68,7 @@ defmodule Stream do
its double. In this example, the list was enumerated just once!
That's what we meant when we said earlier that streams are composable,
lazy enumerables. Note that we could call `Stream.map/2` multiple times,
lazy enumerables. Notice we could call `Stream.map/2` multiple times,
effectively composing the streams and keeping them lazy. The computations
are only performed when you call a function from the `Enum` module.
+40 -89
View File
@@ -60,7 +60,7 @@ defmodule String do
Note it is generally not advised to use `\xNN` in Elixir
strings, as introducing an invalid byte sequence would
make the string invalid. If you have to introduce a
character by its hexadecimal representation, it is best
character by its hexdecimal representation, it is best
to work with Unicode code points, such as `\uNNNN`. In fact,
understanding Unicode code points can be essential when doing
low-level manipulations of string, so let's explore them in
@@ -193,15 +193,14 @@ defmodule String do
## Integer code points
Although code points are represented as integers, this module
represents code points in their encoded format as strings.
For example:
Although code points could be represented as integers, this
module represents all code points as strings. For example:
iex> String.codepoints("olá")
["o", "l", "á"]
There are a couple of ways to retrieve the character code point.
One may use the `?` construct:
There are a couple of ways to retrieve a character integer
code point. One may use the `?` construct:
iex> ?o
111
@@ -221,9 +220,9 @@ defmodule String do
iex> "ol\u00E1"
"olá"
Finally, to convert a String into a list of integer
code points, known as "charlists" in Elixir, you can call
`String.to_charlist`:
Finally, to convert a String into a list of integers
code points, usually known as "char lists", you can call
`Strig.to_charlist`:
iex> String.to_charlist("olá")
[111, 108, 225]
@@ -248,7 +247,7 @@ defmodule String do
## Compile binary patterns
Many functions in this module work with patterns. For example,
`String.split/3` can split a string into multiple strings given
`String.split/2` can split a string into multiple strings given
a pattern. This pattern can be a string, a list of strings or
a compiled pattern:
@@ -283,7 +282,7 @@ defmodule String do
@typedoc "Multiple code points that may be perceived as a single character by readers"
@type grapheme :: t
@typedoc "Pattern used in functions like `replace/4` and `split/3`"
@typedoc "Pattern used in functions like `replace/3` and `split/2`"
@type pattern :: t | [t] | :binary.cp()
@conditional_mappings [:greek]
@@ -374,10 +373,9 @@ defmodule String do
@doc ~S"""
Divides a string into parts based on a pattern.
Returns a list of these parts.
The `pattern` may be a string, a list of strings, a regular expression, or a
compiled pattern.
Returns a list of these parts. The pattern can
be a string, a list of strings, a regular expression,
or a compiled pattern.
The string is split into as many parts as possible by
default, but can be controlled via the `:parts` option.
@@ -681,15 +679,9 @@ defmodule String do
form identified by `form`.
Invalid Unicode codepoints are skipped and the remaining of
the string is converted. If you want the algorithm to stop
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`,
`:unicode.characters_to_nfc_binary/1`, `:unicode.characters_to_nfkd_binary/1`,
and `:unicode.characters_to_nfkc_binary/1` instead.
Normalization forms `:nfkc` and `:nfkd` should not be blindly applied
to arbitrary text. Because they erase many formatting distinctions,
they will prevent round-trip conversion to and from many legacy
character sets.
the string is converted. If you want the algorith to stop
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`
and `:unicode.characters_to_nfc_binary/1` instead.
## Forms
@@ -703,14 +695,6 @@ defmodule String do
* `:nfc` - Normalization Form Canonical Composition.
Characters are decomposed and then recomposed by canonical equivalence.
* `:nfkd` - Normalization Form Compatibility Decomposition.
Characters are decomposed by compatibility equivalence, and
multiple combining characters are arranged in a specific
order.
* `:nfkc` - Normalization Form Compatibility Composition.
Characters are decomposed and then recomposed by compatibility equivalence.
## Examples
iex> String.normalize("yêṩ", :nfd)
@@ -719,12 +703,6 @@ defmodule String do
iex> String.normalize("leña", :nfc)
"leña"
iex> String.normalize("fi", :nfkd)
"fi"
iex> String.normalize("fi", :nfkc)
"fi"
"""
def normalize(string, form)
@@ -742,20 +720,6 @@ defmodule String do
end
end
def normalize(string, :nfkd) do
case :unicode.characters_to_nfkd_binary(string) do
string when is_binary(string) -> string
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkd)
end
end
def normalize(string, :nfkc) do
case :unicode.characters_to_nfkc_binary(string) do
string when is_binary(string) -> string
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkc)
end
end
@doc """
Converts all characters in the given string to uppercase according to `mode`.
@@ -1316,12 +1280,12 @@ defmodule String do
end
defp pad(kind, string, count, padding) do
string_length = length(string)
string_len = length(string)
if string_length >= count do
if string_len >= count do
string
else
filler = build_filler(count - string_length, padding, padding, 0, [])
filler = build_filler(count - string_len, padding, padding, 0, [])
case kind do
:leading -> [filler | string]
@@ -1357,26 +1321,26 @@ defmodule String do
@doc false
@deprecated "Use String.pad_leading/2 instead"
def rjust(subject, length) do
rjust(subject, length, ?\s)
def rjust(subject, len) do
rjust(subject, len, ?\s)
end
@doc false
@deprecated "Use String.pad_leading/3 with a binary padding instead"
def rjust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
pad(:leading, subject, length, [<<pad::utf8>>])
def rjust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
pad(:leading, subject, len, [<<pad::utf8>>])
end
@doc false
@deprecated "Use String.pad_trailing/2 instead"
def ljust(subject, length) do
ljust(subject, length, ?\s)
def ljust(subject, len) do
ljust(subject, len, ?\s)
end
@doc false
@deprecated "Use String.pad_trailing/3 with a binary padding instead"
def ljust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
pad(:trailing, subject, length, [<<pad::utf8>>])
def ljust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
pad(:trailing, subject, len, [<<pad::utf8>>])
end
@doc ~S"""
@@ -1385,8 +1349,7 @@ defmodule String do
The `subject` is always a string.
The `pattern` may be a string, a list of strings, a regular expression, or a
compiled pattern.
The `pattern` may be a string, a regular expression, or a compiled pattern.
The `replacement` may be a string or a function that receives the matched
pattern and must return the replacement as a string or iodata.
@@ -1421,7 +1384,7 @@ defmodule String do
iex> String.replace("a,b,c", ~r/,(.)/, ",\\1\\g{1}")
"a,bb,cc"
Note that we had to escape the backslash escape character (i.e., we used `\\N`
Notice we had to escape the backslash escape character (i.e., we used `\\N`
instead of just `\N` to escape the backslash; same thing for `\\g{N}`). By
giving `\0`, one can inject the whole match in the replacement string.
@@ -1596,13 +1559,10 @@ defmodule String do
:binary.copy(subject, n)
end
@doc ~S"""
Returns a list of code points encoded as strings.
@doc """
Returns all code points in the string.
To retrieve code points in their natural integer
representation, see `to_charlist/1`. For details about
code points and graphemes, see the `String` module
documentation.
For details about code points and graphemes, see the `String` module documentation.
## Examples
@@ -1804,9 +1764,6 @@ defmodule String do
iex> String.next_grapheme("olá")
{"o", "lá"}
iex> String.next_grapheme("")
nil
"""
@spec next_grapheme(t) :: {grapheme, t} | nil
def next_grapheme(binary) do
@@ -1817,9 +1774,9 @@ defmodule String do
end
@doc """
Returns the size (in bytes) of the next grapheme.
Returns the size of the next grapheme.
The result is a tuple with the next grapheme size in bytes and
The result is a tuple with the next grapheme size and
the remainder of the string or `nil` in case the string
reached its end.
@@ -1828,9 +1785,6 @@ defmodule String do
iex> String.next_grapheme_size("olá")
{1, "lá"}
iex> String.next_grapheme_size("")
nil
"""
@spec next_grapheme_size(t) :: {pos_integer, t} | nil
defdelegate next_grapheme_size(string), to: String.Unicode
@@ -1847,9 +1801,6 @@ defmodule String do
iex> String.first("եոգլի")
"ե"
iex> String.first("")
nil
"""
@spec first(t) :: grapheme | nil
def first(string) do
@@ -1943,7 +1894,8 @@ defmodule String do
end
@doc """
Returns a substring starting at the offset `start`, and of the given `length`.
Returns a substring starting at the offset `start`, and of
length `len`.
If the offset is greater than string length, then it returns `""`.
@@ -1984,22 +1936,22 @@ defmodule String do
""
end
def slice(string, start, length) when start >= 0 and length >= 0 do
def slice(string, start, len) when start >= 0 and len >= 0 do
case String.Unicode.split_at(string, start) do
{_, nil} ->
""
{start_bytes, rest} ->
{len_bytes, _} = String.Unicode.split_at(rest, length)
{len_bytes, _} = String.Unicode.split_at(rest, len)
binary_part(string, start_bytes, len_bytes)
end
end
def slice(string, start, length) when start < 0 and length >= 0 do
def slice(string, start, len) when start < 0 and len >= 0 do
start = length(string) + start
case start >= 0 do
true -> slice(string, start, length)
true -> slice(string, start, len)
false -> ""
end
end
@@ -2603,7 +2555,6 @@ defmodule String do
[eq: "fox ", del: "ho", ins: "jum", eq: "ps over the ", del: "dog", ins: "lazy cat"]
"""
@doc since: "1.3.0"
@spec myers_difference(t, t) :: [{:eq | :ins | :del, t}]
def myers_difference(string1, string2) do
graphemes(string1)
+13 -14
View File
@@ -68,7 +68,7 @@ defmodule Supervisor do
Supervisor.count_children(pid)
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
Note that when starting the GenServer, we are registering it
Notice that when starting the GenServer, we are registering it
with name `Stack`, which allows us to call it directly and get
what is on the stack:
@@ -108,7 +108,7 @@ defmodule Supervisor do
The child specification describes how the supervisor starts, shuts down,
and restarts child processes.
The child specification is a map containing up to 6 elements. The first two keys
The child specification is a map which contains 6 elements. The first two keys
in the following list are required, and the remaining ones are optional:
* `:id` - any term used to identify the child specification
@@ -194,7 +194,7 @@ defmodule Supervisor do
start: {Stack, :start_link, [[:hello]]}
}
The map above defines a child with `:id` of `Stack` that is started
The map above defines a supervisor with `:id` of `Stack` that is started
by calling `Stack.start_link([:hello])`.
However, specifying the child specification for each child as a map can be
@@ -433,7 +433,7 @@ defmodule Supervisor do
restarts in transient mode, and linked processes exit with the same
reason unless they're trapping exits
Note that the supervisor that reaches maximum restart intensity will exit with
Notice that the supervisor that reaches maximum restart intensity will exit with
`:shutdown` reason. In this case the supervisor will only be restarted if its
child specification was defined with the `:restart` option set to `:permanent`
(the default).
@@ -823,11 +823,10 @@ defmodule Supervisor do
end
def start_child(supervisor, args) when is_list(args) do
IO.warn_once(
{__MODULE__, :start_child},
"Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead",
_stacktrace_drop_levels = 2
)
# TODO: Deprecate in v1.11
# IO.warn(
# "Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead"
# )
call(supervisor, {:start_child, args})
end
@@ -854,9 +853,10 @@ defmodule Supervisor do
def terminate_child(supervisor, child_id)
def terminate_child(supervisor, pid) when is_pid(pid) do
IO.warn(
"Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
)
# TODO: Deprecate in v1.11
# IO.warn(
# "Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
# )
call(supervisor, {:terminate_child, pid})
end
@@ -929,8 +929,7 @@ defmodule Supervisor do
"""
@spec which_children(supervisor) :: [
# inlining module() | :dynamic here because :supervisor.modules() is not exported
{term() | :undefined, child | :restarting, :worker | :supervisor, module() | :dynamic}
{term() | :undefined, child | :restarting, :worker | :supervisor, :supervisor.modules()}
]
def which_children(supervisor) do
call(supervisor, :which_children)
+11 -9
View File
@@ -41,10 +41,10 @@ defmodule Supervisor.Spec do
end
end
Note that in this case we don't have to explicitly import
`Supervisor.Spec` since `use Supervisor` automatically does so.
Notice in this case we don't have to explicitly import
`Supervisor.Spec` as `use Supervisor` automatically does so.
Defining a module-based supervisor can be useful, for example,
to perform initialization tasks in the `c:Supervisor.init/1` callback.
to perform initialization tasks in the `c:init/1` callback.
## Supervisor and worker options
@@ -82,9 +82,9 @@ defmodule Supervisor.Spec do
terminates abnormally, i.e., with an exit reason other than
`:normal`, `:shutdown` or `{:shutdown, term}`
Note that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
In this case the supervisor will only restart if its child specification was defined with
the `:restart` option set to `:permanent` (the default).
Notice that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
In this case the supervisor will only be restarted if its child specification was defined with
the `:restart` option is set to `:permanent` (the default).
### Shutdown values (`:shutdown`)
@@ -136,7 +136,7 @@ defmodule Supervisor.Spec do
supervise and a set of `options`.
Returns a tuple containing the supervisor specification. This tuple can be
used as the return value of the `c:Supervisor.init/1` callback when implementing a
used as the return value of the `c:init/1` callback when implementing a
module-based supervisor.
## Examples
@@ -234,7 +234,8 @@ defmodule Supervisor.Spec do
function: atom,
modules: modules
) :: spec
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
# TODO: Deprecate on v1.11
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
def worker(module, args, options \\ []) do
child(:worker, module, args, options)
end
@@ -268,7 +269,8 @@ defmodule Supervisor.Spec do
function: atom,
modules: modules
) :: spec
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
# TODO: Deprecate on v1.11
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
def supervisor(module, args, options \\ []) do
options = Keyword.put_new(options, :shutdown, :infinity)
child(:supervisor, module, args, options)
+19 -41
View File
@@ -312,14 +312,9 @@ defmodule System do
"""
@spec user_home() :: String.t() | nil
def user_home do
case :init.get_argument(:home) do
{:ok, [[home] | _]} ->
encoding = :file.native_name_encoding()
:unicode.characters_to_binary(home, encoding, encoding)
_ ->
nil
end
{:ok, [[home] | _]} = :init.get_argument(:home)
encoding = :file.native_name_encoding()
:unicode.characters_to_binary(home, encoding, encoding)
end
@doc """
@@ -398,9 +393,8 @@ defmodule System do
@doc """
Registers a program exit handler function.
Registers a function that will be invoked at the end of an Elixir script.
A script is typically started via the command line via the `elixir` and
`mix` executables.
Registers a function that will be invoked at the end of program execution.
Useful for invoking a hook in "script" mode.
The handler always executes in a different process from the one it was
registered in. As a consequence, any resources managed by the calling process
@@ -408,9 +402,6 @@ defmodule System do
function is invoked.
The function must receive the exit status code as an argument.
If the VM terminates programmatically, via `System.stop/1` or `System.halt/1`,
the `at_exit/1` callbacks are not executed.
"""
@spec at_exit((non_neg_integer -> any)) :: :ok
def at_exit(fun) when is_function(fun, 1) do
@@ -591,20 +582,15 @@ defmodule System do
`__STACKTRACE__/0` inside a rescue/catch. If you want to support
earlier Elixir versions, move `System.stacktrace/0` inside a rescue/catch.
Starting from Erlang/OTP 23, this function will always return an empty list.
Note that the Erlang VM (and therefore this function) does not
return the current stacktrace but rather the stacktrace of the
latest exception. To retrieve the stacktrace of the current process,
use `Process.info(self(), :current_stacktrace)` instead.
"""
# TODO: Once Erlang/OTP 23 is required, remove conditional, and update @doc accordingly.
# The warning is emitted by the compiler - so a @doc annotation is enough
@doc deprecated: "Use __STACKTRACE__ instead"
if function_exported?(:erlang, :get_stacktrace, 0) do
def stacktrace, do: apply(:erlang, :get_stacktrace, [])
else
def stacktrace, do: []
# TODO: Fully deprecate it on Elixir v1.11 via @deprecated
# It is currently partially deprecated in elixir_dispatch.erl
def stacktrace do
apply(:erlang, :get_stacktrace, [])
end
@doc """
@@ -637,7 +623,6 @@ defmodule System do
System.halt(:abort)
"""
@spec halt() :: no_return
@spec halt(non_neg_integer | binary | :abort) :: no_return
def halt(status \\ 0)
@@ -753,12 +738,7 @@ defmodule System do
* `:into` - injects the result into the given collectable, defaults to `""`
* `:cd` - the directory to run the command in
* `:env` - an enumerable of tuples containing environment key-value as
binary. The child process inherits all environment variables from its
parent process, the Elixir application, except those overwritten or
cleared using this option. Specify a value of `nil` to clear (unset) an
environment variable, which is useful for preventing credentials passed
to the application from leaking into child processes.
* `:env` - an enumerable of tuples containing environment key-value as binary
* `:arg0` - sets the command arg0
* `:stderr_to_stdout` - redirects stderr to stdout when `true`
* `:parallelism` - when `true`, the VM will schedule port tasks to improve
@@ -988,7 +968,6 @@ defmodule System do
Inlined by the compiler.
"""
@spec os_time() :: integer
@doc since: "1.3.0"
def os_time do
:os.system_time()
end
@@ -1000,7 +979,6 @@ defmodule System do
with no limitation and is not monotonic.
"""
@spec os_time(time_unit) :: integer
@doc since: "1.3.0"
def os_time(unit) do
:os.system_time(normalize_time_unit(unit))
end
@@ -1009,7 +987,6 @@ defmodule System do
Returns the Erlang/OTP release number.
"""
@spec otp_release :: String.t()
@doc since: "1.3.0"
def otp_release do
:erlang.list_to_binary(:erlang.system_info(:otp_release))
end
@@ -1018,7 +995,6 @@ defmodule System do
Returns the number of schedulers in the VM.
"""
@spec schedulers :: pos_integer
@doc since: "1.3.0"
def schedulers do
:erlang.system_info(:schedulers)
end
@@ -1027,7 +1003,6 @@ defmodule System do
Returns the number of schedulers online in the VM.
"""
@spec schedulers_online :: pos_integer
@doc since: "1.3.0"
def schedulers_online do
:erlang.system_info(:schedulers_online)
end
@@ -1096,12 +1071,15 @@ defmodule System do
end
defp warn(unit, replacement_unit) do
IO.warn_once(
{__MODULE__, unit},
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
_stacktrace_drop_levels = 4
)
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
stacktrace = Enum.drop(stacktrace, 3)
:elixir_config.warn({System, unit}, stacktrace) &&
IO.warn(
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
stacktrace
)
replacement_unit
end
+11 -149
View File
@@ -203,9 +203,6 @@ defmodule Task do
a list `[pid_n, ..., pid2, pid1]` with at least one entry Where `pid_n` is
the PID that called the current process, `pid2` called `pid_n`, and `pid2` was
called by `pid1`.
If a task crashes, the callers field is included as part of the log message
metadata under the `:callers` key.
"""
@doc """
@@ -461,8 +458,8 @@ defmodule Task do
This is also useful when you're using the tasks for side effects.
Defaults to `true`.
* `:timeout` - the maximum amount of time (in milliseconds or `:infinity`)
each task is allowed to execute for. Defaults to `5000`.
* `:timeout` - the maximum amount of time (in milliseconds) each
task is allowed to execute for. Defaults to `5000`.
* `:on_timeout` - what to do when a task times out. The possible
values are:
@@ -491,38 +488,6 @@ defmodule Task do
stream = Task.async_stream(collection, Mod, :expensive_fun, [], ordered: false)
Stream.run(stream)
## Attention: async + take
Given items in an async stream are processed concurrently, doing
`async_stream` followed by `Enum.take/2` may cause more items than
requested to be processed. Let's see an example:
1..100
|> Task.async_stream(fn i ->
Process.sleep(100)
IO.puts(to_string(i))
end)
|> Enum.take(10)
For a machine with 8 cores, the above will process 16 items instead
of 10. The reason is that `async_stream/5` always have 8 elements
processing at once. So by the time `Enum` says it got all elements
it needed, there are still 6 elements left to be processed.
The solution here is to use `Stream.take/2` instead of `Enum.take/2`
to filter elements before-hand:
1..100
|> Stream.take(10)
|> Task.async_stream(fn i ->
Process.sleep(100)
IO.puts(to_string(i))
end)
|> Enum.to_list()
If for some reason you cannot take the elements before hand,
you can use `:max_concurrency` to limit how many elements
may be over processed at the cost of reducing concurrency.
"""
@doc since: "1.4.0"
@spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t()
@@ -593,12 +558,11 @@ defmodule Task do
In case the task process dies, the current process will exit with the same
reason as the task.
A timeout, in milliseconds or `:infinity`, can be given with a default value
of `5000`. If the timeout is exceeded, then the current process will exit. If
the task process is linked to the current process which is the case when a
task is started with `async`, then the task process will also exit. If the
task process is trapping exits or not linked to the current process, then it
will continue to run.
A timeout in milliseconds or `:infinity`, can be given with a default value of `5000`. If the
timeout is exceeded, then the current process will exit. If the task process
is linked to the current process which is the case when a task is started with
`async`, then the task process will also exit. If the task process is trapping
exits or not linked to the current process, then it will continue to run.
This function assumes the task's monitor is still active or the monitor's
`:DOWN` message is in the message queue. If it has been demonitored, or the
@@ -644,109 +608,6 @@ defmodule Task do
end
end
@doc """
Awaits replies from multiple tasks and returns them.
This function receives a list of tasks and waits for their replies in the
given time interval. It returns a list of the results, in the same order as
the tasks supplied in the `tasks` input argument.
If any of the task processes dies, the current process will exit with the
same reason as that task.
A timeout, in milliseconds or `:infinity`, can be given with a default value
of `5000`. If the timeout is exceeded, then the current process will exit.
Any task processes that are linked to the current process (which is the case
when a task is started with `async`) will also exit. Any task processes that
are trapping exits or not linked to the current process will continue to run.
This function assumes the tasks' monitors are still active or the monitors'
`:DOWN` message is in the message queue. If any tasks have been demonitored,
or the message already received, this function will wait for the duration of
the timeout.
This function can only be called once for any given task. If you want to be
able to check multiple times if a long-running task has finished its
computation, use `yield_many/2` instead.
## Compatibility with OTP behaviours
It is not recommended to `await` long-running tasks inside an OTP behaviour
such as `GenServer`. See `await/2` for more information.
## Examples
iex> tasks = [
...> Task.async(fn -> 1 + 1 end),
...> Task.async(fn -> 2 + 3 end)
...> ]
iex> Task.await_many(tasks)
[2, 5]
"""
@doc since: "1.11.0"
@spec await_many([t], timeout) :: [term]
def await_many(tasks, timeout \\ 5000) when is_timeout(timeout) do
awaiting =
for task <- tasks, into: %{} do
%Task{ref: ref, owner: owner} = task
if owner != self() do
raise ArgumentError, invalid_owner_error(task)
end
{ref, true}
end
timeout_ref = make_ref()
timer_ref =
if timeout != :infinity do
Process.send_after(self(), timeout_ref, timeout)
end
try do
await_many(tasks, timeout, awaiting, %{}, timeout_ref)
after
timer_ref && Process.cancel_timer(timer_ref)
receive do: (^timeout_ref -> :ok), after: (0 -> :ok)
end
end
defp await_many(tasks, _timeout, awaiting, replies, _timeout_ref)
when map_size(awaiting) == 0 do
for %{ref: ref} <- tasks, do: Map.fetch!(replies, ref)
end
defp await_many(tasks, timeout, awaiting, replies, timeout_ref) do
receive do
^timeout_ref ->
demonitor_pending_tasks(awaiting)
exit({:timeout, {__MODULE__, :await_many, [tasks, timeout]}})
{:DOWN, ref, _, proc, reason} when is_map_key(awaiting, ref) ->
demonitor_pending_tasks(awaiting)
exit({reason(reason, proc), {__MODULE__, :await_many, [tasks, timeout]}})
{ref, reply} when is_map_key(awaiting, ref) ->
Process.demonitor(ref, [:flush])
await_many(
tasks,
timeout,
Map.delete(awaiting, ref),
Map.put(replies, ref, reply),
timeout_ref
)
end
end
defp demonitor_pending_tasks(awaiting) do
Enum.each(awaiting, fn {ref, _} ->
Process.demonitor(ref, [:flush])
end)
end
@doc false
@deprecated "Pattern match directly on the message instead"
def find(tasks, {ref, reply}) when is_reference(ref) do
@@ -786,9 +647,10 @@ defmodule Task do
* the caller is trapping exits
A timeout, in milliseconds or `:infinity`, can be given with a default value
of `5000`. If the time runs out before a message from the task is received,
this function will return `nil` and the monitor will remain active. Therefore
`yield/2` can be called multiple times on the same task.
of `5000`. If the time runs out before a message from
the task is received, this function will return `nil`
and the monitor will remain active. Therefore `yield/2` can be
called multiple times on the same task.
This function assumes the task's monitor is still active or the
monitor's `:DOWN` message is in the message queue. If it has been
+1 -1
View File
@@ -269,7 +269,7 @@ defmodule Task.Supervised do
receive do
# The task at position "position" replied with "value". We put the
# response in the "waiting" map and do nothing, since we'll only act on
# this response when the replying task dies (we'll see this in the :down
# this response when the replying task dies (we'll notice in the :down
# message).
{{^monitor_ref, position}, reply} ->
%{^position => {pid, :running}} = waiting
+2 -4
View File
@@ -27,10 +27,8 @@ defmodule Task.Supervisor do
@typedoc "Option values used by `start_link`"
@type option ::
DynamicSupervisor.option()
# :permanent | :transient | :temporary here because :supervisor.restart() is not exported
| {:restart, :permanent | :transient | :temporary}
# :brutal_kill | timeout() here because :supervisor.shutdown() is not exported
| {:shutdown, :brutal_kill | timeout()}
| {:restart, :supervisor.restart()}
| {:shutdown, :supervisor.shutdown()}
@doc false
def child_spec(opts) when is_list(opts) do
+3 -3
View File
@@ -60,7 +60,7 @@ defmodule Tuple do
"""
@spec duplicate(term, non_neg_integer) :: tuple
def duplicate(data, size) when is_integer(size) and size >= 0 do
def duplicate(data, size) do
:erlang.make_tuple(size, data)
end
@@ -83,7 +83,7 @@ defmodule Tuple do
"""
@spec insert_at(tuple, non_neg_integer, term) :: tuple
def insert_at(tuple, index, value) when is_integer(index) and index >= 0 do
def insert_at(tuple, index, value) do
:erlang.insert_element(index + 1, tuple, value)
end
@@ -124,7 +124,7 @@ defmodule Tuple do
"""
@spec delete_at(tuple, non_neg_integer) :: tuple
def delete_at(tuple, index) when is_integer(index) and index >= 0 do
def delete_at(tuple, index) do
:erlang.delete_element(index + 1, tuple)
end
+1 -1
View File
@@ -204,7 +204,7 @@ defmodule URI do
next_pair =
case :binary.split(undecoded_next_pair, "=") do
[key, value] -> {decode_www_form(key), decode_www_form(value)}
[key] -> {decode_www_form(key), ""}
[key] -> {decode_www_form(key), nil}
end
{next_pair, rest}
+201 -116
View File
@@ -95,9 +95,7 @@ defmodule Version do
"""
import Kernel, except: [match?: 2]
@enforce_keys [:major, :minor, :patch]
defstruct [:major, :minor, :patch, :build, pre: []]
defstruct [:major, :minor, :patch, :pre, :build]
@type version :: String.t() | t
@type requirement :: String.t() | Version.Requirement.t()
@@ -118,92 +116,36 @@ defmodule Version do
for more information.
"""
defstruct [:source, :lexed]
defstruct [:source, :matchspec, :compiled]
@opaque t :: %__MODULE__{
source: String.t(),
lexed: [atom | matchable]
matchspec: :ets.match_spec() | :ets.comp_match_spec(),
compiled: boolean
}
@typep matchable ::
{Version.major(), Version.minor(), Version.patch(), Version.pre(), Version.build()}
@compile inline: [compare: 2]
@doc false
@spec new(String.t(), :ets.match_spec()) :: t
def new(source, lexed) do
%__MODULE__{source: source, lexed: lexed}
def new(source, spec) do
%__MODULE__{source: source, matchspec: spec, compiled: false}
end
@doc false
@spec compile(t) :: t
def compile(%__MODULE__{matchspec: spec} = requirement) do
%{requirement | matchspec: :ets.match_spec_compile(spec), compiled: true}
end
@doc false
@spec match?(t, tuple) :: boolean
def match?(%__MODULE__{lexed: lexed}, matchable_pattern) do
match_lexed?(lexed, matchable_pattern)
def match?(%__MODULE__{matchspec: spec, compiled: true}, matchable_pattern) do
matches = :ets.match_spec_run([matchable_pattern], spec)
matches != []
end
defp match_lexed?([operator, req, :&& | rest], version) do
match_op?(operator, req, version) and match_lexed?(rest, version)
end
defp match_lexed?([operator, req, :|| | rest], version) do
match_op?(operator, req, version) or match_lexed?(rest, version)
end
defp match_lexed?([operator, req], version) do
match_op?(operator, req, version)
end
defp match_op?(:==, req, version) do
compare(version, req) == :eq
end
defp match_op?(:!=, req, version) do
compare(version, req) != :eq
end
defp match_op?(:~>, {major, minor, nil, req_pre, _}, {_, _, _, pre, allow_pre} = version) do
compare(version, {major, minor, 0, req_pre, nil}) in [:eq, :gt] and
compare(version, {major + 1, 0, 0, [0], nil}) == :lt and
(allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:~>, {major, minor, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
compare(version, req) in [:eq, :gt] and
compare(version, {major, minor + 1, 0, [0], nil}) == :lt and
(allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:>, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
compare(version, req) == :gt and (allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:>=, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
compare(version, req) in [:eq, :gt] and (allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:<, req, version) do
compare(version, req) == :lt
end
defp match_op?(:<=, req, version) do
compare(version, req) in [:eq, :lt]
end
defp compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do
cond do
major1 > major2 -> :gt
major1 < major2 -> :lt
minor1 > minor2 -> :gt
minor1 < minor2 -> :lt
patch1 > patch2 -> :gt
patch1 < patch2 -> :lt
pre1 == [] and pre2 != [] -> :gt
pre1 != [] and pre2 == [] -> :lt
pre1 > pre2 -> :gt
pre1 < pre2 -> :lt
true -> :eq
end
def match?(%__MODULE__{matchspec: spec, compiled: false}, matchable_pattern) do
{:ok, result} = :ets.test_ms(matchable_pattern, spec)
result != false
end
end
@@ -328,12 +270,8 @@ defmodule Version do
defp do_compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do
cond do
major1 > major2 -> :gt
major1 < major2 -> :lt
minor1 > minor2 -> :gt
minor1 < minor2 -> :lt
patch1 > patch2 -> :gt
patch1 < patch2 -> :lt
{major1, minor1, patch1} > {major2, minor2, patch2} -> :gt
{major1, minor1, patch1} < {major2, minor2, patch2} -> :lt
pre1 == [] and pre2 != [] -> :gt
pre1 != [] and pre2 == [] -> :lt
pre1 > pre2 -> :gt
@@ -406,8 +344,12 @@ defmodule Version do
@spec parse_requirement(String.t()) :: {:ok, Requirement.t()} | :error
def parse_requirement(string) when is_binary(string) do
case Version.Parser.parse_requirement(string) do
{:ok, lexed} -> {:ok, Requirement.new(string, lexed)}
:error -> :error
{:ok, spec} ->
requirement = Requirement.new(string, spec)
{:ok, requirement}
:error ->
:error
end
end
@@ -428,20 +370,27 @@ defmodule Version do
@doc since: "1.8.0"
@spec parse_requirement!(String.t()) :: Requirement.t()
def parse_requirement!(string) when is_binary(string) do
case parse_requirement(string) do
{:ok, requirement} -> requirement
:error -> raise InvalidRequirementError, string
case Version.Parser.parse_requirement(string) do
{:ok, spec} ->
Requirement.new(string, spec)
:error ->
raise InvalidRequirementError, string
end
end
@doc """
Compiles a requirement to an internal representation that may optimize matching.
Compiles a requirement to its internal representation with
`:ets.match_spec_compile/1` for faster matching.
The internal representation is opaque.
The internal representation is opaque and cannot be converted to external
term format and then back again without losing its properties (meaning it
can not be sent to a process on another node and still remain a valid
compiled match_spec, nor can it be stored on disk).
"""
@spec compile_requirement(Requirement.t()) :: Requirement.t()
def compile_requirement(requirement) do
requirement
Requirement.compile(requirement)
end
defp to_matchable(%Version{major: major, minor: minor, patch: patch, pre: pre}, allow_pre?) do
@@ -505,27 +454,13 @@ defmodule Version do
end
def lexer("", acc) do
Enum.map(Enum.reverse(acc), fn
op when is_atom(op) ->
op
version when is_binary(version) ->
case Version.Parser.parse_version(version, true) do
{:ok, version} -> version
:error -> :error
end
end)
Enum.reverse(acc)
end
@spec parse_requirement(String.t()) :: {:ok, term} | :error
def parse_requirement(source) do
lexed = lexer(source, [])
if valid_requirement?(lexed) do
{:ok, lexed}
else
:error
end
to_matchspec(lexed)
end
def parse_version(string, approximate? \\ false) when is_binary(string) do
@@ -615,7 +550,7 @@ defmodule Version do
defp valid_requirement?([a | next]), do: valid_requirement?(a, next)
# it must finish with a version
defp valid_requirement?(a, []) when is_tuple(a) do
defp valid_requirement?(a, []) when is_binary(a) do
true
end
@@ -625,29 +560,179 @@ defmodule Version do
end
# <version> or | <version> and
defp valid_requirement?(a, [b | next]) when is_tuple(a) and is_atom(b) and b in [:||, :&&] do
defp valid_requirement?(a, [b | next]) when is_binary(a) and is_atom(b) and b in [:||, :&&] do
valid_requirement?(b, next)
end
# or <version> | and <version>
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_tuple(b) and a in [:||, :&&] do
valid_requirement?(b, next)
end
# ~> <version>
defp valid_requirement?(:~>, [b | next]) when is_tuple(b) do
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) and a in [:||, :&&] do
valid_requirement?(b, next)
end
# <op> <version>
defp valid_requirement?(a, [{_major, _minor, patch, _pre, _build} = b | next])
when is_atom(a) and is_integer(patch) do
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) do
valid_requirement?(b, next)
end
defp valid_requirement?(_, _) do
false
end
defp approximate_upper(version) do
case version do
{major, _minor, nil, _} ->
{major + 1, 0, 0, [0]}
{major, minor, _patch, _} ->
{major, minor + 1, 0, [0]}
end
end
defp to_matchspec(lexed) do
if valid_requirement?(lexed) do
first = to_condition(lexed)
rest = Enum.drop(lexed, 2)
{:ok, [{{:"$1", :"$2", :"$3", :"$4", :"$5"}, [to_condition(first, rest)], [:"$_"]}]}
else
:error
end
catch
:invalid_matchspec -> :error
end
defp to_condition([:==, version | _]) do
matchable = parse_condition(version)
main_condition(:==, matchable)
end
defp to_condition([:!=, version | _]) do
matchable = parse_condition(version)
main_condition(:"/=", matchable)
end
defp to_condition([:~>, version | _]) do
from = parse_condition(version, true)
to = approximate_upper(from)
{
:andalso,
to_condition([:>=, matchable_to_string(from)]),
to_condition([:<, matchable_to_string(to)])
}
end
defp to_condition([:>, version | _]) do
{major, minor, patch, pre} = parse_condition(version)
{
:andalso,
{
:orelse,
main_condition(:>, {major, minor, patch}),
{:andalso, main_condition(:==, {major, minor, patch}), pre_condition(:>, pre)}
},
no_pre_condition(pre)
}
end
defp to_condition([:>=, version | _]) do
matchable = parse_condition(version)
{:orelse, main_condition(:==, matchable), to_condition([:>, version])}
end
defp to_condition([:<, version | _]) do
{major, minor, patch, pre} = parse_condition(version)
{
:orelse,
main_condition(:<, {major, minor, patch}),
{:andalso, main_condition(:==, {major, minor, patch}), pre_condition(:<, pre)}
}
end
defp to_condition([:<=, version | _]) do
matchable = parse_condition(version)
{:orelse, main_condition(:==, matchable), to_condition([:<, version])}
end
defp to_condition(current, []) do
current
end
defp to_condition(current, [:&&, operator, version | rest]) do
to_condition({:andalso, current, to_condition([operator, version])}, rest)
end
defp to_condition(current, [:||, operator, version | rest]) do
to_condition({:orelse, current, to_condition([operator, version])}, rest)
end
defp parse_condition(version, approximate? \\ false) do
case parse_version(version, approximate?) do
{:ok, {major, minor, patch, pre, _build}} -> {major, minor, patch, pre}
:error -> throw(:invalid_matchspec)
end
end
defp main_condition(op, version) when tuple_size(version) == 3 do
{op, {{:"$1", :"$2", :"$3"}}, {:const, version}}
end
defp main_condition(op, version) when tuple_size(version) == 4 do
{op, {{:"$1", :"$2", :"$3", :"$4"}}, {:const, version}}
end
defp pre_condition(:>, pre) do
length_pre = length(pre)
{
:orelse,
{:andalso, {:==, {:length, :"$4"}, 0}, {:const, length_pre != 0}},
{
:andalso,
{:const, length_pre != 0},
{
:orelse,
{:>, {:length, :"$4"}, length_pre},
{:andalso, {:==, {:length, :"$4"}, length_pre}, {:>, :"$4", {:const, pre}}}
}
}
}
end
defp pre_condition(:<, pre) do
length_pre = length(pre)
{
:orelse,
{:andalso, {:"/=", {:length, :"$4"}, 0}, {:const, length_pre == 0}},
{
:andalso,
{:"/=", {:length, :"$4"}, 0},
{
:orelse,
{:<, {:length, :"$4"}, length_pre},
{:andalso, {:==, {:length, :"$4"}, length_pre}, {:<, :"$4", {:const, pre}}}
}
}
}
end
defp no_pre_condition([]) do
{:orelse, :"$5", {:==, {:length, :"$4"}, 0}}
end
defp no_pre_condition(_pre) do
{:const, true}
end
defp matchable_to_string({major, minor, patch, pre}) do
patch = if patch, do: "#{patch}", else: "0"
pre = if pre != [], do: "-#{Enum.join(pre, ".")}"
"#{major}.#{minor}.#{patch}#{pre}"
end
end
end
@@ -8,7 +8,6 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.11 | Development
1.10 | Bug fixes and security patches
1.9 | Security patches only
1.8 | Security patches only
@@ -54,7 +53,6 @@ Elixir version | Supported Erlang/OTP versions
1.8 | 20 - 22
1.9 | 20 - 22
1.10 | 21 - 22 (and Erlang/OTP 23 from v1.10.3)
1.11 | 21 - 23
While Elixir often adds compatibility to new Erlang/OTP versions on released branches, such as support for Erlang/OTP 20 in v1.4.5, those releases usually contain the minimum changes for Elixir to run without errors. Only the next minor release, in this case v1.5.0, does effectively leverage the new features provided by the latest Erlang/OTP release.
@@ -76,10 +74,6 @@ The first column is the version the feature was hard deprecated. The second colu
Version | Deprecated feature | Replaced by (available since)
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
[v1.11] | `Mix.Project.compile/2` | `Mix.Task.run("compile", args)` (v1.0)
[v1.11] | `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` | The new child specs outlined in `Supervisor` (v1.5)
[v1.11] | `Supervisor.start_child/2` and `Supervisor.terminate_child/2` | `DynamicSupervisor` (v1.6)
[v1.11] | `System.stacktrace/1` | `__STACKTRACE__` in `try/catch/rescue` (v1.7)
[v1.10] | `Code.ensure_compiled?/1` | `Code.ensure_compiled/1` (v1.0)
[v1.10] | `Code.load_file/2` | `Code.require_file/2` (v1.0) or `Code.compile_file/2` (v1.7)
[v1.10] | `Code.loaded_files/0` | `Code.required_files/0` (v1.7)
@@ -110,7 +104,9 @@ Version | Deprecated feature | Replaced by (ava
[v1.7] | `Registry.start_link/3` | `Registry.start_link/1` (v1.5)
[v1.7] | `Stream.chunk/2,3,4` | `Stream.chunk_every/2` and [`Stream.chunk_every/3,4`](`Stream.chunk_every/4`) (v1.5)
[v1.6] | `Enum.partition/2` | `Enum.split_with/2` (v1.4)
[v1.6] | `Keyword.replace/3` | `Keyword.fetch/2` + `Keyword.put/3` (v1.0)
[v1.6] | `Macro.unescape_tokens/1,2` | Use `Enum.map/2` to traverse over the arguments (v1.0)
[v1.6] | `Map.replace/3` | `Map.fetch/2` + `Map.put/3` (v1.0)
[v1.6] | `Module.add_doc/6` | [`@doc`](`Module`) module attribute (v1.0)
[v1.6] | `Range.range?/1` | Pattern match on [`_.._`](`Kernel.../2`) (v1.0)
[v1.5] | `()` to mean `nil` | `nil` (v1.0)
@@ -174,4 +170,3 @@ Version | Deprecated feature | Replaced by (ava
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
[v1.11]: https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md#4-hard-deprecations
@@ -8,7 +8,7 @@ You can create a new Elixir library by running the `mix new` command:
$ mix new my_library
The project name is given in the `snake_case` convention where all letters are lowercase and words are separate with underscores. This is the same convention used by variables, function names and atoms in Elixir. See the [Naming Conventions](naming-conventions.md) document for more information.
The project name is given in the `snake_case` convention where all letters are lowercase and words are separate with underscores. This is the same convention used by variables, function names and atoms in Elixir. See the [Naming Conventions](naming-conventions.html) document for more information.
Every project has a `mix.exs` file, with instructions on how to build, compile, run tests, and so on. Libraries commonly have a `lib` directory, which includes Elixir source code, and a `test` directory. A `src` directory may also exist for Erlang sources.
@@ -30,7 +30,7 @@ Writing code is only the first of many steps to publish a package. We strongly r
* Write tests. Elixir ships with a test-framework named [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html). The project generated by `mix new` includes sample tests and doctests.
* Write documentation. The Elixir community is proud of treating documentation as a first-class citizen and making documentation easily accessible. Libraries contribute to the status quo by providing complete API documentation with examples for their modules, types and functions. See the [Writing Documentation](writing-documentation.md) guide for more information. Projects like [ExDoc](https://github.com/elixir-lang/ex_doc) can be used to generate HTML and EPUB documents from the documentation. ExDoc also supports "extra pages", like this one that you are reading. Such pages augment the documentation with tutorials, guides and references.
* Write documentation. The Elixir community is proud of treating documentation as a first-class citizen and making documentation easily accessible. Libraries contribute to the status quo by providing complete API documentation with examples for their modules, types and functions. See the [Writing Documentation](writing-documentation.html) guide for more information. Projects like [ExDoc](https://github.com/elixir-lang/ex_doc) can be used to generate HTML and EPUB documents from the documentation. ExDoc also supports "extra pages", like this one that you are reading. Such pages augment the documentation with tutorials, guides and references.
Projects are often made available to other developers [by publishing a Hex package](https://hex.pm/docs/publish). Hex also [supports private packages for organizations](https://hex.pm/pricing). If ExDoc is configured for the Mix project, publishing a package on Hex will also automatically publish the generated documentation to [HexDocs](https://hexdocs.pm).
@@ -71,7 +71,7 @@ More examples of paired functions: `Base.decode16/2` and `Base.decode16!/2`, `Fi
There are also some non-paired functions, with no non-bang variant. The bang still signifies that it will raise an exception on failure. Example: `Protocol.assert_protocol!/1`.
In macro code, the bang on `Kernel.alias!/1` and `Kernel.var!/2` signifies that [macro hygiene](https://elixir-lang.org/getting-started/meta/macros.html#macro-hygiene) is set aside.
In macro code, the bang on `Kernel.alias!/1` and `Kernel.var!/2` signifies that [macro hygiene](https://elixir-lang.org/getting-started/meta/macros.html#macros-hygiene) is set aside.
## Trailing question mark (`foo?`)
@@ -89,7 +89,7 @@ Examples: `Integer.is_even/1`, `Kernel.is_list/1`
These functions and macros follow the Erlang convention of an `is_` prefix, instead of a trailing question mark, precisely to indicate that they are allowed in guard clauses.
Note that type checks that are not valid in guard clauses do not follow this convention. For example: `Keyword.keyword?/1`.
Note that type checks that are not valid in guard clauses do not follow this convention. Examples: `Keyword.keyword?/1`, `Regex.regex?/1`
## Special names
@@ -13,7 +13,7 @@ Operator
`+` `-` `!` `^` `not` `~~~` | Unary
`*` `/` | Left to right
`+` `-` | Left to right
`++` `--` `..` `<>` `+++` `---` | Right to left
`++` `--` `..` `<>` | Right to left
`^^^` | Left to right
`in` `not in` | Left to right
`\|>` `<<<` `>>>` `<<~` `~>>` `<~` `~>` `<~>` `<\|>` | Left to right
@@ -29,55 +29,18 @@ Operator
`when` | Right to left
`<-` `\\` | Left to right
## General operators
Elixir provides the following built-in operators that are defined as functions that can be overridden:
* [`+`](`+/1`) and [`-`](`-/1`) - unary positive/negative
* [`+`](`+/2`), [`-`](`-/2`), [`*`](`*/2`), and [`/`](`//2`) - basic arithmetic operations
* [`++`](`++/2`) and [`--`](`--/2`) - list concatenation and subtraction
* [`and`](`and/2`) and [`&&`](`&&/2`) - strict and relaxed boolean "and"
* [`or`](`or/2`) and [`||`](`||/2`) - strict and relaxed boolean "or"
* [`not`](`not/1`) and [`!`](`!/1`) - strict and relaxed boolean "not"
* [`in`](`in/2`) and [`not in`](`in/2`) - membership
* [`@`](`@/1`) - module attribute
* [`..`](`../2`) - range creation
* [`<>`](`<>/2`) - binary concatenation
* [`|>`](`|>/2`) - pipeline
* [`=~`](`=~/2`) - text-based match
Many of those can be used in guards; consult the [list of allowed guard functions and operators](patterns-and-guards.md#list-of-allowed-functions-and-operators).
Additionally, there are a few other operators that Elixir parses but doesn't actually use.
See [Custom and overridden operators](#custom-and-overridden-operators) below for a list and for guidelines about their use.
Some other operators are special forms and cannot be overridden:
* [`^`](`^/1`) - pin operator
* [`.`](`./2`) - dot operator
* [`=`](`=/2`) - match operator
* [`&`](`&/1`) - capture operator
* [`::`](`Kernel.SpecialForms.::/2`) - type operator
Finally, these operators appear in the precedence table above but are only meaningful within certain constructs:
* `=>` - see [`%{}`](`%{}/1`)
* `when` - see [Guards](patterns-and-guards.md#guards)
* `<-` - see [`for`](`for/1`) and [`with`](`with/1`)
* `\\` - see [Default arguments](Kernel.html#def/2-default-arguments)
## Comparison operators
Elixir provides the following built-in comparison operators (all of which can be used in guards):
Elixir provides the following built-in comparison operators:
* [`==`](`==/2`) - equal to
* [`===`](`===/2`) - strictly equal to
* [`!=`](`!=/2`) - inequal to
* [`!==`](`!==/2`) - strictly inequal to
* [`<`](`</2`) - less-than
* [`>`](`>/2`) - greater-than
* [`<=`](`<=/2`) - less-than or equal to
* [`>=`](`>=/2`) - greater-than or equal to
* [`==`](`==/2`) - equality
* [`===`](`===/2`) - strict equality
* [`!=`](`!=/2`) - inequality
* [`!==`](`!==/2`) - strict inequality
* [`<`](`</2`) - less than
* [`>`](`>/2`) - greater than
* [`<=`](`<=/2`) - less than or equal
* [`>=`](`>=/2`) - greater than or equal
The only difference between [`==`](`==/2`) and [`===`](`===/2`) is that [`===`](`===/2`) is strict when it comes to comparing integers and floats:
@@ -113,7 +76,6 @@ The collection types are compared using the following rules:
* 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.
## Custom and overridden operators
@@ -145,6 +107,7 @@ iex> 1 <~ 2
The following is a table of all the operators that Elixir is capable of parsing, but that are not used by default:
* `|`
* `|||`
* `&&&`
* `<<<`
@@ -156,8 +119,6 @@ The following is a table of all the operators that Elixir is capable of parsing,
* `<~>`
* `<|>`
* `^^^`
* `+++`
* `---`
* `~~~`
The following operators are used by the `Bitwise` module when imported: [`&&&`](`Bitwise.&&&/2`), [`^^^`](`Bitwise.^^^/2`), [`<<<`](`Bitwise.<<</2`), [`>>>`](`Bitwise.>>>/2`), [`|||`](`Bitwise.|||/2`), [`~~~`](`Bitwise.~~~/1`). See the documentation for `Bitwise` for more information.
@@ -6,7 +6,7 @@ This page describes the semantics of patterns and guards, where they are all all
## Patterns
Patterns in Elixir are made of variables, literals, and data-structure specific syntax. One of the most used constructs to perform pattern matching is the match operator ([`=`](`=/2`)):
Patterns in Elixir are made of variables, literals, and data-structure specific syntax. One of the most used constructs to perform pattern matching is the match operator (`=`):
```iex
iex> x = 1
@@ -83,7 +83,7 @@ Atoms and numbers (integers and floats) can appear in patterns and they are alwa
```iex
iex> :atom = :atom
:atom
:atotm
iex> :atom = :another_atom
** (MatchError) no match of right hand side value: :another_atom
```
@@ -156,7 +156,7 @@ iex> [head | tail] = []
** (MatchError) no match of right hand side value: []
```
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator ([`++`](`++/2`)):
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator (`++`):
```elixir
iex> 'hello ' ++ world = 'hello world'
@@ -207,7 +207,7 @@ Finally, note map keys in patterns must always be literals or previously bound v
### Binaries
Binaries may appear in patterns using the double less-than/greater-than syntax ([`<<>>`](`<<>>/1`)). A binary in a pattern can match multiple segments at the same, each with different type, size, and unit:
Binaries may appear in patterns using the double less-than/greater-than syntax (`<<>>`). A binary in a pattern can match multiple segments at the same, each with different type, size, and unit:
```iex
iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
@@ -216,9 +216,9 @@ iex> val
31544
```
See the documentation for [`<<>>`](`<<>>/1`) for a complete definition of pattern matching for binaries.
See the documentation for `<<>>` for a complete definition of pattern matching for binaries.
Finally, remember that strings in Elixir are UTF-8 encoded binaries. This means that, similar to charlists, prefix matches on strings are also possible with the binary concatenation operator ([`<>`](`<>/2`)):
Finally, remember that strings in Elixir are UTF-8 encoded binaries. This means that, similar to charlists, prefix matches on strings are also possible with the binary concatenation operator (`<>`):
```elixir
iex> "hello " <> world = "hello world"
@@ -231,7 +231,7 @@ Suffix matches (`hello <> " world"`) are not valid patterns.
## Guards
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and others.
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and ohers.
Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere. It also allows the compiler to optimize the code related to guards efficiently.
@@ -282,7 +282,7 @@ not_nil_head?(["some_value", "another_value"])
Even though the head of the list is not `nil`, the first clause for `not_nil_head?/1` fails because the expression does not evaluate to `true`, but to `"some_value"`, therefore triggering the second clause which returns `false`. To make the guard behave correctly, you must ensure that the guard evaluates to `true`, like so:
```elixir
def not_nil_head?([head | _]) when head != nil, do: true
def not_nil_head?(term) when head != nil, do: true
def not_nil_head?(_), do: false
not_nil_head?(["some_value", "another_value"])
@@ -309,7 +309,7 @@ iex> case "hello" do
...> _anything_else ->
...> :failed
...> end
:failed
:worked
```
In many cases, we can take advantage of this. In the code above, we used `tuple_size/1` to both check that the given value is a tuple *and* check its size (instead of using `is_tuple(something) and tuple_size(something) == 2`).
@@ -380,7 +380,13 @@ Check.empty?({})
## Where patterns and guards can be used
In the examples above, we have used the match operator ([`=`](`=/2`)) and function clauses to showcase patterns and guards respectively. Here is the list of the built-in constructs in Elixir that support patterns and guards.
In the examples above, we have used the match operator (`=`) and function clauses to showcase patterns and guards respectively. Here is the list of the built-in constructs in Elixir that support patterns and guards.
* the match operator (`=`) (exceptionally does not support guards):
```elixir
{:ok, binary} = File.read("some/file")
```
* `match?/2`:
@@ -424,16 +430,8 @@ In the examples above, we have used the match operator ([`=`](`=/2`)) and functi
* [`try`](`try/1`) supports patterns and guards on `catch` and `else`
* [`receive`](`receive/1`) supports patterns and guards to match on the received messages.
* custom guards can also be defined with `defguard/1` and `defguardp/1`. A custom guard can only be defined based on existing guards.
Note that the match operator ([`=`](`=/2`)) does *not* support guards:
```elixir
{:ok, binary} = File.read("some/file")
```
## Custom patterns and guards expressions
Only the constructs listed in this page are allowed in patterns and guards. However, we can take advantage of macros to write custom patterns guards that can simplify our programs or make them more domain-specific. At the end of the day, what matters is that the *output* of the macros boils down to a combination of the constructs above.
@@ -21,7 +21,7 @@ Integers (`1234`) and floats (`123.4`) in Elixir are represented as a sequence o
### Atoms
Unquoted atoms start with a colon (`:`) which must be immediately followed by an underscore or a Unicode letter. The atom may continue using a sequence of Unicode letters, numbers, underscores, and `@`. Atoms may end in `!` or `?`. See [Unicode Syntax](unicode-syntax.md) for a formal specification. Valid unquoted atoms are: `:ok`, `:ISO8601`, and `:integer?`.
Unquoted atoms start with a colon (`:`) which must be immediately followed by an underscore or a Unicode letter. The atom may continue using a sequence of Unicode letters, numbers, underscores, and `@`. Atoms may end in `!` or `?`. See [Unicode Syntax](unicode-syntax.html) for a formal specification. Valid unquoted atoms are: `:ok`, `:ISO8601`, and `:integer?`.
If the colon is immediately followed by a pair of double- or single-quotes surrounding the atom name, the atom is considered quoted. In contrast with an unquoted atom, this one can be made of any Unicode character (not only letters), such as `:'🌢 Elixir'`, `:"++olá++"`, and `:"123"`.
@@ -84,27 +84,27 @@ Structs built on the map syntax by passing the struct name between `%` and `{`.
### Variables
Variables in Elixir must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The variable may continue using a sequence of Unicode letters, numbers, and underscores. Variables may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
Variables in Elixir must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The variable may continue using a sequence of Unicode letters, numbers, and underscores. Variables may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
[Elixir's naming conventions](naming-conventions.md) recommend variables to be in `snake_case` format.
[Elixir's naming conventions](naming-conventions.html) recommend variables to be in `snake_case` format.
### Non-qualified calls (local calls)
Non-qualified calls, such as `add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscore. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
Non-qualified calls, such as `add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscore. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
Parentheses for non-qualified calls are optional, except for zero-arity calls, which would then be ambiguous with variables. If parentheses are used, they must immediately follow the function name *without spaces*. For example, `add (1, 2)` is a syntax error, since `(1, 2)` is treated as an invalid block which is attempted to be given as a single argument to `add`.
[Elixir's naming conventions](naming-conventions.md) recommend calls to be in `snake_case` format.
[Elixir's naming conventions](naming-conventions.html) recommend calls to be in `snake_case` format.
### Operators
As many programming languages, Elixir also support operators as non-qualified calls with their precedence and associativity rules. Constructs such as `=`, `when`, `&` and `@` are simply treated as operators. See [the Operators page](operators.md) for a full reference.
As many programming languages, Elixir also support operators as non-qualified calls with their precedence and associativity rules. Constructs such as `=`, `when`, `&` and `@` are simply treated as operators. See [the Operators page](operators.html) for a full reference.
### Qualified calls (remote calls)
Qualified calls, such as `Math.add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscores. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
Qualified calls, such as `Math.add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscores. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
[Elixir's naming conventions](naming-conventions.md) recommend calls to be in `snake_case` format.
[Elixir's naming conventions](naming-conventions.html) recommend calls to be in `snake_case` format.
For qualified calls, Elixir also allows the function name to be written between double- or single-quotes, allowing calls such as `Math."++add++"(1, 2)`. Operators can be used as qualified calls without a need for quote, such as `Kernel.+(1, 2)`.
@@ -114,7 +114,7 @@ Parentheses for qualified calls are optional. If parentheses are used, they must
Aliases are constructs that expand to atoms at compile-time. The alias `String` expands to the atom `:"Elixir.String"`. Aliases must start with an ASCII uppercase character which may be followed by any ASCII letter, number, or underscore. Non-ASCII characters are not supported in aliases.
[Elixir's naming conventions](naming-conventions.md) recommend aliases to be in `CamelCase` format.
[Elixir's naming conventions](naming-conventions.html) recommend aliases to be in `CamelCase` format.
### Blocks
@@ -234,7 +234,7 @@ end
#=> {:+, [], [1, 2]}
```
Note that `.` is also an operator. Remote calls use the dot in the AST with two arguments, where the second argument is always an atom:
Notice that `.` is also an operator. Remote calls use the dot in the AST with two arguments, where the second argument is always an atom:
```elixir
quote do
@@ -104,7 +104,6 @@ The following literals are also supported in typespecs:
## Maps
| %{} # empty map
| %{key: value_type} # map with required key :key of value_type
| %{key_type => value_type} # map with required pairs of key_type and value_type
| %{required(key_type) => value_type} # map with required pairs of key_type and value_type
| %{optional(key_type) => value_type} # map with optional pairs of key_type and value_type
| %SomeStruct{} # struct with all fields of any type
@@ -163,7 +162,7 @@ A map value does not belong to this type if it contains a key that is not in the
If you want to denote that keys that were not previously defined in the map are allowed,
it is common to end a map type with `optional(any) => any`.
Note that the syntactic representation of `map()` is `%{optional(any) => any}`, not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
Notice that the syntactic representation of `map()` is `%{optional(any) => any}`, not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
### User-defined types
@@ -260,7 +259,7 @@ Elixir's standard library contains a few frequently used behaviours such as `Gen
### Inspecting behaviours
The `@callback` and `@optional_callbacks` attributes are used to create a `behaviour_info/1` function available on the defining module. This function can be used to retrieve the callbacks and optional callbacks defined by that module.
The `@callback` and `@optional_callback` attributes are used to create a `behaviour_info/1` function available on the defining module. This function can be used to retrieve the callbacks and optional callbacks defined by that module.
For example, for the `MyBehaviour` module defined in "Optional callbacks" above:

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