Compare commits

..
74 Commits
Author SHA1 Message Date
José Valim 56f1037973 Release v1.3.2 2016-07-15 18:49:26 +02:00
Alex e5fcef3832 Make sure to return nil if enumerable halts
Fix issue #5004

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-15 10:55:47 +02:00
José Valim 9081a7aad2 Update VERSION 2016-07-13 00:15:40 +02:00
Eric Meadows-Jönsson 328f581ef5 Add MIX_NO_DEPS env var for disabling dep loading
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-12 17:51:37 +02:00
Eksperimental aa695875fe Standardize mix escript task's shortdoc (#4984)
Correct verb tense, and remove trailing period.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-12 17:51:37 +02:00
Aleksei Magusev aa039ce9f9 Fix edit script for maps when there are no joint elements
Closes #4977.
2016-07-11 01:19:30 +02:00
Eric Entin 2fdf96e747 The else clause in with supports guards (#4960)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-07 20:26:52 +02:00
José Valim 494bded12a deps.check -> deps.loadpaths
deps.loadpaths was public API that Nerves, in
particular, depended on. This commit brings
back the deps.loadpaths task, replacing the
incorrect deps.check that should not be required
by Nerves.
2016-07-05 21:07:11 +02:00
José Valim 2b0fc529ee Improve error messages for :only conflict
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-04 20:58:01 +02:00
James Fish aaa775a3bf Fix @macrocallback spec translation
Signed-off-by: James Fish <james@fishcakez.com>
2016-07-04 18:21:25 +01:00
Andrea Leopardi 6647b00fcf Fix the spec for OptionParser.parse_head!/2
Signed-off-by: Andrea Leopardi <an.leopardi@gmail.com>
2016-07-04 00:04:49 +02:00
Andrea Leopardi d149c4c1e0 Polish and fix docs for OptionParser (#4924)
I also

* fixed some style in just a couple of lines of code
* renamed a couple of variables for clarity

Signed-off-by: Andrea Leopardi <an.leopardi@gmail.com>
2016-07-04 00:03:45 +02:00
Justin Schneck 58810bd33c add filter option to deps.unlock (#4932)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 22:08:09 +02:00
sunboshan 050cb45664 Fix typespec for Kernel.pop_in/2 (#4931)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 22:06:52 +02:00
Eric Entin 7f59aeeb6f mix test --listen-on-stdin will no longer exit on failure (#4930)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 19:12:53 +02:00
José Valim e2abd35b3c Ensure missing protocol dependencies are discarded, closes #4842
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 00:01:52 +02:00
Aleksei Magusev cc35c8fc22 Actually hide to_char_list/1 2016-07-02 00:34:33 +03:00
Eric Entin 2718631eb6 mix test --listen-on-stdin (#4921)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-01 21:02:04 +02:00
José Valim 9d9aa8bff8 Disable --warnings-as-errors for deps
See #4913
2016-06-30 22:26:35 +01:00
Eric Meadows-Jönsson 65aee00809 Add IS_DEP=1 env var when compiling make deps
When IS_DEP=1 erlang.mk disables warnings as errors.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:22:58 +01:00
Eric Meadows-Jönsson 9f028834d7 Reenable dialyzer reaise test
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:18:46 +01:00
Eric Meadows-Jönsson 44ef53ec2b Allow custom fields in Exception.t typespec
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:18:39 +01:00
Eric Meadows-Jönsson d1290d62db Fix struct/0 typespec
Closes #4915.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:18:32 +01:00
Mike Stok 301aa6eddc keep tokenizer's column counts in sync for numbers with _ characters
For providing better feedback in credo and tools like it we should be
able to map a token back to its original source.  This makes sure that
`_` characters in numbers are properly accounted for so that they stay
in sync after we've encountered something like 123_456_789.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 10:31:13 +01:00
José Valim 098b2d7cfd Ensure Access.at/1 properly pop list elements, closes #4911
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 10:31:10 +01:00
José Valim d92b7a331b Traverse dependnecies when consolidate umbrella protocols, closes #4842
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-29 01:21:18 +01:00
José Valim 8930ea7598 Do not attempt to load modules that have not been required, closes #4900 2016-06-29 01:20:45 +01:00
José Valim 01a1acd27b Release v1.3.1 2016-06-28 10:52:54 +01:00
José Valim 127691d4ad Ensure structs can be expanded in dynamic module names, closes #4894
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-28 10:42:34 +01:00
José Valim e848e87f52 Do not load modules for xref purposes, instead use BEAM info
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-28 01:00:26 +02:00
Daniel Perez bcadd96e31 Revert mix do to enforce space after comma (#4893)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-27 22:05:52 +02:00
Eric Entin 99a0bfa074 Skip/fix tests on OTP 19 (#4855)
* Callback-related tests use beam AST to find callbacks

* Fix invalid function call warning on OTP 19

* Skip dialyzer-related tests on OTP 19

* Fix IEx h helper for OTP 19

* Improve IEx h helper test for multi-arity callbacks

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-27 00:02:43 +02:00
Andrea Leopardi 5d00232633 Fix an assertion in Kernel.TypespecTest
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-26 19:34:50 +02:00
Andrea Leopardi c319bc450e Support module attributes in remote types (#4891)
For example, @for.t in protocols or similar use cases.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-26 19:34:46 +02:00
José Valim a9ed654ae2 Update CHANGELOG 2016-06-25 01:09:21 +02:00
Aleksei Magusev 6dacf53268 Fix Kernel.LexicalTracker not to discard alias directives on import
Closes #4871.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 01:05:46 +02:00
Aleksei Magusev 5e22b12272 Validate application properties before altering them
Closes #4873.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 01:05:26 +02:00
José Valim d4501893c7 Add import_file_if_available, closes #4878
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 00:48:02 +02:00
Benjamin Falk 744859ee42 Generate error for no pid given to Genserver.reply (#4876)
Prior to this change it is possible to send any two-pair tuple to
`GenServer.reply/2` and it will happily return `:ok`, catching any
errors.  This leads to some hard to track-down bugs if you pass
it the wrong term.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 00:40:56 +02:00
Henrik Nyh 4596baab3f Add IEx.Helpers.import_if_available/2 (#4877)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 00:40:46 +02:00
Eksperimental c065041263 Add missing backquote in Module docs (#4866)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-23 23:44:50 +02:00
Daniel Perez 95bff23ede Fix h helper for binary operators, closes #4859 (#4860)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-23 10:11:06 +02:00
Marcus Gartner 0bc04195f2 Clarify Enum.group_by/3 deprecation message (#4852)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 19:02:04 +02:00
José Valim d195b75aa3 deps.check does not check archives 2016-06-22 10:54:28 +02:00
José Valim dc06a18287 Reenable deps.check on loadpaths dry-run
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 10:48:52 +02:00
José Valim 54c0574be2 Explicitly announce structs
Before this patch, we could announce a struct too early,
only when __struct__/0 was defined (and not __struct__/1)
leading to deadlocks in the compiler. We fixed the bug and
made struct announcements to avoid future bugs.

Closes #4844

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 10:05:13 +02:00
Daniel Perez 8d50e1fa95 Improve with/else error message on invalid form (#4847)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 09:33:28 +02:00
José Valim e70d9723eb Check for proper Makefile when compiling on Windows, closes #4841 2016-06-21 22:17:35 +02:00
José Valim 3ce029615d Do not expect stacktraces to be always present, closes #4840
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-21 20:27:01 +02:00
José Valim 416bf41a03 Release v1.3.0 2016-06-21 12:25:14 +02:00
José Valim c758c5e401 Update docs for c/1 and c/2 2016-06-21 12:14:04 +02:00
Aleksei Magusev 7b4576f0fc Consider variables are on different lines in assert_receive 2016-06-20 23:34:55 +02:00
José Valim 9192907e68 Do not leak external variables used in guards in assert_receive 2016-06-20 14:45:30 +02:00
José Valim 5533cc4665 Expose schedulers_online and otp_release information 2016-06-16 23:51:55 +02:00
Eric Entin 873dbc16ff more mix xref doc improvements (#4818)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 21:44:21 +02:00
José Valim 84c91ef914 Improve mix xref docs
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 21:08:05 +02:00
Eric Entin a977afcd6c mix xref graph (#4811)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 20:58:29 +02:00
Aleksei Magusev 8ed30ad926 Do not use deprecated String.strip
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 20:58:20 +02:00
Krzysztof Wende 8496d52df5 yecc instead of yeec (#4813)
Typo. should be `yecc` instead of `yeec` in the documentation

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 20:58:15 +02:00
José Valim 977bb418a1 Ensure variables in function clauses are not reused, closes #4815 2016-06-15 16:14:37 +02:00
José Valim 735296a4ab Remove elixir_counter in favor of unique_integer 2016-06-15 15:25:04 +02:00
José Valim e34dde79ea Ensure entries are sorted to avoid non-determinism in compilation, closes #4814 2016-06-15 15:13:31 +02:00
José Valim 1eb11374c3 Update CHANGELOG 2016-06-12 23:53:07 +02:00
Eric Entin 9c2ec6a260 mix xref command-style (#4808)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 23:52:25 +02:00
Eric Entin 92cf02321b Make sure struct expansion results in a remote dispatch (#4807)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 23:52:09 +02:00
Aleksei Magusev 2b5e69aa05 Make sure with/1 does not leak variables to else (#4804)
Conflicts:
	lib/elixir/src/elixir_scope.erl
2016-06-12 21:39:20 +02:00
Aleksei Magusev a56606d5d6 Do not silence unused variable warnings in assert_receive
Closes #4789. It's not needed anymore after e190474955 commit.
2016-06-12 21:39:20 +02:00
José Valim b2329d11a8 Use explicit call for deps() 2016-06-12 18:29:04 +02:00
Felipe Seré 88b3e864d5 Clean deps when deps.compile is run with --force (#4780)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 10:00:24 +02:00
Josh Price efaca6c3e9 More informative message for DOT tree generation (#4791)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 10:00:19 +02:00
José Valim 67aabe0bac Calculate beam files only when writing manifest data
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-11 15:38:24 +02:00
José Valim 8506cc68bf Ensure conflict with umbrella children are shown, closes #4792
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-11 15:38:24 +02:00
Myron Marston a1bbc1bc20 Document change in behavior of Enum.group_by ordering. (#4794)
On 1.2, here's how `group_by` behaved:

    iex(1)> 1..10 |> Enum.group_by(fn i -> i < 5 end)
    %{false: [10, 9, 8, 7, 6, 5], true: [4, 3, 2, 1]}

On 1.3, here's how it behaves:

    iex(1)> 1..10 |> Enum.group_by(fn i -> i < 5 end)
    %{false: [5, 6, 7, 8, 9, 10], true: [1, 2, 3, 4]}
2016-06-09 20:40:11 +02:00
José Valim 18dfca21f0 Update doc references 2016-06-08 21:14:15 +02:00
691 changed files with 55039 additions and 201078 deletions
-50
View File
@@ -1,50 +0,0 @@
env:
CIRRUS_CLONE_DEPTH: 50
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_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_freebsd_task:
<<: *DEFAULT_TEST_SETTINGS
name: FreeBSD 13.0
alias: FreeBSD Stable
freebsd_instance:
image_family: freebsd-13-0
cpu: 8
memory: 7424Mi
env:
CHECK_REPRODUCIBLE: true
LC_ALL: en_US.UTF-8
PATH: $PATH:/usr/local/lib/erlang24/bin
install_script:
- pkg install -y erlang-runtime24 git gmake
- rm -rf .git
- gmake compile
build_info_script: bin/elixir --version
test_formatted_script:
- gmake 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: gmake test_erlang
test_elixir_script: gmake test_elixir
check_reproducible_script: |
if [ -n "$CHECK_REPRODUCIBLE" ]; then
gmake check_reproducible
else
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
fi
-19
View File
@@ -1,19 +0,0 @@
[
inputs: [
"lib/*/{lib,unicode,test}/**/*.{ex,exs}",
"lib/*/*.exs",
"lib/ex_unit/examples/*.exs",
".formatter.exs"
],
locals_without_parens: [
# Formatter tests
assert_format: 2,
assert_format: 3,
assert_same: 1,
assert_same: 2,
# Errors tests
assert_eval_raise: 3
],
normalize_bitstring_modifiers: false
]
-2
View File
@@ -1,3 +1 @@
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
*.ex diff=elixir
*.exs diff=elixir
-11
View File
@@ -1,11 +0,0 @@
---
blank_issues_enabled: true
contact_links:
- name: Discuss proposals
url: https://groups.google.com/g/elixir-lang-core
about: Send proposals for new ideas to our mailing list
- name: Ask questions and support
url: https://elixirforum.com/
about: Ask questions, provide support and more on Elixir Forum
-53
View File
@@ -1,53 +0,0 @@
---
name: Report an issue
description:
Tell us about something that is not working the way we (probably) intend
body:
- type: markdown
attributes:
value: >
Thank you for contributing to Elixir! :heart:
Please, do not use this form for guidance, questions or support.
Try instead in [Elixir Forum](https://elixirforum.com),
the [IRC Chat](https://web.libera.chat/#elixir),
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
[Slack](https://elixir-slackin.herokuapp.com),
[Discord](https://discord.gg/elixir) or in other online communities.
- type: textarea
id: elixir-and-otp-version
attributes:
label: Elixir and Erlang/OTP versions
description: Paste the output of `elixir --version` here.
validations:
required: true
- type: input
id: os
attributes:
label: Operating system
description: The operating system that this issue is happening on.
validations:
required: true
- type: textarea
id: current-behavior
attributes:
label: Current behavior
description: >
Include code samples, errors, and stacktraces if appropriate.
If reporting a bug, please include the reproducing steps.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A short description on how you expect the code to behave.
validations:
required: true
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
-129
View File
@@ -1,129 +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
permissions:
contents: read
jobs:
test_linux:
name: Linux, ${{ matrix.otp_release }}, Ubuntu 20.04
strategy:
fail-fast: false
matrix:
include:
- otp_release: OTP-25.0
otp_latest: true
- otp_release: OTP-24.3
- otp_release: OTP-24.0
- otp_release: OTP-23.3
- otp_release: OTP-23.0
- otp_release: master
development: true
- otp_release: maint
development: true
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 50
- name: Install Erlang/OTP
run: |
cd $RUNNER_TEMP
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-20.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 "$(pwd)/otp/bin" >> $GITHUB_PATH
- name: Compile Elixir
run: |
rm -rf .git
make compile
echo "$PWD/bin" >> $GITHUB_PATH
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Run Dialyzer
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
continue-on-error: ${{ matrix.development }}
- name: Elixir test suite
run: make test_elixir
continue-on-error: ${{ matrix.development }}
- name: Check reproducible builds
run: taskset 1 make check_reproducible
if: ${{ matrix.otp_latest }}
- name: Build docs
if: ${{ matrix.otp_latest }}
run: |
git config --global advice.detachedHead false
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
for branch in main v${EX_DOC_LATEST_STABLE_VERSION}; do
echo "Building docs with ExDoc ${branch}"
cd ..
git clone https://github.com/elixir-lang/ex_doc.git --branch ${branch} --depth 1
cd ex_doc
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
cd ../elixir/
make docs
rm -rf ../ex_doc/
done
test_windows:
name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019
strategy:
matrix:
otp_release: ['23.3']
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@v3
with:
fetch-depth: 50
- name: Cache Erlang/OTP package
uses: actions/cache@v3
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: |
del c:/Windows/System32/drivers/etc/hosts
make --keep-going test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
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"
-72
View File
@@ -1,72 +0,0 @@
# #!/usr/bin/env elixir
[tag] = System.argv()
Mix.install([
{:req, "~> 0.2.1"},
{:jason, "~> 1.0"}
])
%{status: 200, body: release} =
Req.get!("https://api.github.com/repos/elixir-lang/elixir/releases/tags/#{tag}")
if release["draft"] do
raise "cannot notify a draft release"
end
## Notify on elixir-lang-ann
names_and_checksums =
for asset <- release["assets"],
name = asset["name"],
name =~ ~r/.sha\d+sum$/,
do: {name, Req.get!(asset["browser_download_url"]).body}
line_items =
for {name, checksum_and_name} <- Enum.sort(names_and_checksums) do
[checksum | _] = String.split(checksum_and_name, " ")
root = Path.rootname(name)
"." <> type = Path.extname(name)
" * #{root} - #{type} - #{checksum}\n"
end
mail = %{
"From" => "jose.valim@dashbit.co",
"To" => "elixir-lang-ann@googlegroups.com",
"Subject" => "Elixir #{tag} released",
"HtmlBody" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{line_items}",
"MessageStream" => "outbound"
}
if System.get_env("DRYRUN") do
IO.puts("MAIL")
IO.inspect(mail)
else
headers = %{
"X-Postmark-Server-Token" => System.fetch_env!("ELIXIR_LANG_ANN_TOKEN")
}
resp = Req.post!("https://api.postmarkapp.com/email", {:json, mail}, headers: headers)
IO.puts("#{resp.status} elixir-lang-ann\n#{inspect(resp.body)}")
end
## Notify on Elixir Forum
post = %{
"title" => "Elixir #{tag} released",
"raw" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{release["body"]}",
# Elixir News
"category" => 28
}
if System.get_env("DRYRUN") do
IO.puts("POST")
IO.inspect(post)
else
headers = %{
"api-key" => System.fetch_env!("ELIXIR_FORUM_TOKEN"),
"api-username" => "Elixir"
}
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
end
-28
View File
@@ -1,28 +0,0 @@
name: Notify
on:
release:
types:
- published
permissions:
contents: read
jobs:
notify:
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: |
elixir .github/workflows/notify.exs ${{ github.ref_name }}
-96
View File
@@ -1,96 +0,0 @@
name: Release
on:
push:
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
jobs:
create_draft_release:
permissions:
contents: write
runs-on: ubuntu-20.04
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
run: |
gh release create \
--repo ${{ github.repository }} \
--title ${{ github.ref_name }} \
--notes '' \
--draft \
${{ github.ref_name }}
release_pre_built:
needs: create_draft_release
strategy:
fail-fast: true
matrix:
include:
- otp: 23
otp_version: 23.3
- otp: 24
otp_version: 24.3
- otp: 25
otp_version: 25.0
build_docs: build_docs
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp_version }}
version-type: strict
- name: Build Elixir Release
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ matrix.otp }}.zip
shasum -a 1 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH
- name: Upload Pre-built
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload --clobber "${{ github.ref_name }}" \
elixir-otp-${{ matrix.otp }}.zip \
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum
- name: Get latest stable ExDoc version
if: ${{ matrix.build_docs }}
run: |
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
- uses: actions/checkout@v3
if: ${{ matrix.build_docs }}
with:
repository: elixir-lang/ex_doc
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
path: ex_doc
- name: Build ex_doc
if: ${{ matrix.build_docs }}
run: |
mv ex_doc ../ex_doc
cd ../ex_doc
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
cd ../elixir
- name: Build Docs
if: ${{ matrix.build_docs }}
run: |
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: Upload Docs
if: ${{ matrix.build_docs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload --clobber "${{ github.ref_name }}" \
Docs.zip \
Docs.zip.sha{1,256}sum
+9 -5
View File
@@ -1,13 +1,17 @@
/doc/
/lib/*/ebin/
/lib/*/_build/
/lib/*/tmp/
/doc
/ebin
/lib/*/ebin/*
/lib/*/tmp
/lib/elixir/src/*_lexer.erl
/lib/elixir/src/*_parser.erl
/lib/elixir/test/ebin/
/lib/elixir/src/elixir.app.src
/lib/elixir/test/ebin
/man/elixir.1
/man/iex.1
/rel/elixir
/Docs-v*.zip
/Precompiled-v*.zip
/.eunit
/.release
.elixir.plt
erl_crash.dump
+15
View File
@@ -0,0 +1,15 @@
language: erlang
otp_release:
- 18.0
sudo: false
script: "make compile && rm -rf .git && make test"
notifications:
irc: "irc.freenode.org#elixir-lang"
recipients:
- jose.valim@plataformatec.com.br
- eric.meadows.jonsson@gmail.com
+352 -486
View File
@@ -1,600 +1,466 @@
# Changelog for Elixir v1.14
# Changelog for Elixir v1.3
Elixir v1.14 brings many improvements to the debugging experience in Elixir
and data-type inspection. It also includes a new abstraction for easy
partitioning of processes called `PartitionSupervisor`, as well as improved
compilation times and error messages.
Elixir v1.3 brings many improvements to the language, the compiler and its tooling, specially Mix (Elixir's build tool) and ExUnit (Elixir's test framework).
Elixir v1.14 is the last version to support Erlang/OTP 23. Consider updating
to Erlang/OTP 24 or Erlang/OTP 25.
## Language improvements
## `dbg`
The language has been improved semantically and includes new types and APIs. Let's see the three major features.
`Kernel.dbg/2` is a new macro that's somewhat similar to `IO.inspect/2`, but
specifically tailored for **debugging**.
### Deprecation of imperative assignment
When called, it prints the value of whatever you pass to it, plus the debugged
code itself as well as its location. This code:
Elixir will now warn if constructs like `if`, `case` and friends assign to a variable that is accessed in an outer scope. As an example, imagine a function called `format` that receives a message and some options and it must return a path alongside the message:
```elixir
# In my_file.exs
feature = %{name: :dbg, inspiration: "Rust"}
dbg(feature)
dbg(Map.put(feature, :in_version, "1.14.0"))
def format(message, opts) do
path =
if (file = opts[:file]) && (line = opts[:line]) do
relative = Path.relative_to_cwd(file)
message = Exception.format_file_line(relative, line) <> " " <> message
relative
end
{path, message}
end
```
Prints this:
```shell
$ elixir my_file.exs
[my_file.exs:2: (file)]
feature #=> %{inspiration: "Rust", name: :dbg}
[my_file.exs:3: (file)]
Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg}
```
`dbg/2` can do more. It's a macro, so it *understands Elixir code*. You can see
that when you pass a series of `|>` pipes to it. `dbg/2` will print the value
for every step of the pipeline. This code:
The `if` block above is implicitly changing the value in `message`. Now imagine we want to move the `if` block to its own function to clean up the implementation:
```elixir
# In dbg_pipes.exs
__ENV__.file
|> String.split("/", trim: true)
|> List.last()
|> File.exists?()
|> dbg()
def format(message, opts) do
path = with_file_and_line(message, opts)
{path, message}
end
defp with_file_and_line(message, opts) do
if (file = opts[:file]) && (line = opts[:line]) do
relative = Path.relative_to_cwd(file)
message = Exception.format_file_line(relative, line) <> " " <> message
relative
end
end
```
Prints this:
The refactored version is broken because the `if` block was actually returning two values, the relative path *and* the new message. Elixir v1.3 will warn on such cases, forcing both variables to be explicitly returned from `if`, `case` and other constructs. Furthermore, this change gives us the opportunity to unify the language scoping rules in future releases.
```shell
$ elixir dbg_pipes.exs
[dbg_pipes.exs:5: (file)]
__ENV__.file #=> "/home/myuser/dbg_pipes.exs"
|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]
|> List.last() #=> "dbg_pipes.exs"
|> File.exists?() #=> true
### Calendar types and sigils
Elixir v1.3 introduces the `Calendar` module as well as 4 new calendar types:
* `Date` - used to store dates (year, month, day) in a given calendar
* `Time` - used to store time (hour, minute, second, microseconds)
* `NaiveDateTime` - used to store datetimes without a timezone (year, month, day, hour, minute, second, microseconds) in a given calendar. It is called naïve because without a timezone, the datetime may not actually exist. For example, when there are daylight savings changes, a whole hour may not exist (when the clock moves forward) or a particular instant may happen twice (when the clock moves backwards)
* `DateTime` - used to store datetimes with timezone (year, month, day, hour, minute, second, microsecond and time zone, with abbreviation, UTC and standard offset)
The current Calendar modules and its types is to provide a base for interoperatibility in the ecosystem instead of full-featured datetime API. This release includes basic functionality for building new types and converting them from and back strings.
Elixir v1.3 also introduces 3 new sigils related to the types above:
* `~D[2016-05-29]` - builds a new date
* `~T[08:00:00]` and `~T[08:00:00.285]` - builds a new time (with different precisions)
* `~N[2016-05-29 08:00:00]` - builds a naive date time
### Access selectors
This release introduces new accessors to make it simpler for developers to traverse nested data structures, traversing and updating data in different ways. For instance, given a user with a list of languages, here is how to deeply traverse the map and convert all language names to uppercase:
```iex
iex> user = %{name: "john",
...> languages: [%{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}]}
iex> update_in user, [:languages, Access.all(), :name], &String.upcase/1
%{name: "john",
languages: [%{name: "ELIXIR", type: :functional},
%{name: "C", type: :procedural}]}
```
### IEx and Prying
You can see the new accessors in the `Access` module.
`dbg/2` supports configurable backends. IEx automatically replaces the default
backend by one that halts the code execution with `IEx.Pry`, giving developers
the option to access local variables, imports, and more. This also works with
pipelines: if you pass a series of `|>` pipe calls to `dbg` (or pipe into it at the
end, like `|> dbg()`), you'll be able to step through every line in the pipeline.
## Mix
You can keep the default behaviour by passing the `--no-pry` option to IEx.
Mix includes new tasks to improve your everyday workflow. Some of those tasks relies on many compiler improvements to know more about your code, providing static analysis to find possible bugs in your code and faster compilation cycles.
## PartitionSupervisor
### Compiling n files
`PartitionSupervisor` is a new module that implements a new supervisor type. The
partition supervisor is designed to help with situations where you have a single
supervised process that becomes a bottleneck. If that process's state can be
easily partitioned, then you can use `PartitionSupervisor` to supervise multiple
isolated copies of that process running concurrently, each assigned its own
partition.
Mix no longer announces every file it compiles. Instead it outputs how many files there is to compile per compilers. Here is the output for a project like [`gettext`](https://github.com/elixir-lang/gettext):
For example, imagine you have an `ErrorReporter` process that you use to report
errors to a monitoring service.
```
Compiling 1 file (.yrl)
Compiling 1 file (.erl)
Compiling 19 files (.ex)
Generated gettext app
```
In case a file is taking too long to compile, Mix will announce such, for example:
```
Compiling lib/gettext.ex (it's taking more than 5s)
```
The goal of these changes is to put an increased focus on the "warnings" emitted by the compiler.
In any case, the previous behaviour can be brought back with the `--verbose` flag and the compilation threshold for files that are taking long can be set via the `--long-compilation-threshold` option.
### mix xref
Speaking about warnings, Mix v1.3 includes a new task called `xref` that performs cross reference checks in your code. One of such checks is the ability to find calls to modules and functions that do not exist. For example, if in your library code you call `ThisModuleDoesNotExist.foo(1, 2, 3)`, `mix xref unreachable` will be able to find such code and let you know about it.
Since such checks can discover possible bugs in your codebase, a new compiler called `xref` has been added to `Mix.compilers/0`, so they run by default every time you compile your code.
We have included other modes in `xref`, such as `mix xref callers Foo`, to find all places in your code that a function from the module `Foo` is called. We hope other tools and text editors can leverage such features to provide useful functionality for their users.
### Better dependency tracking
Besides `xref`, Elixir v1.3 provides better module tracking generally. For example, in previous versions, if you changed a `:path` dependency, Elixir would always fully recompile the current project. In this release, we have improved the tracking algorithms such that, if you change a `:path` dependency, only the files that depend on such dependency are recompiled.
Such improvements do not only make compilation faster but they also make working with umbrella applications much more productive. Previously, changing a sibling application triggered a full project recompilation, now Elixir can track between sibling applications and recompile only what is needed.
### mix app.tree and deps.tree
Mix also includes both `mix app.tree` and `mix deps.tree`. The first will list all applications your current project needs to start in order to boot (i.e. the ones listed in `application/0` in your `mix.exs`) while the second will lists all of your dependencies and so on recursively.
Here is a quick example from [Plug](https://github.com/elixir-lang/plug):
```elixir
# Application supervisor:
children = [
# ...,
ErrorReporter
]
Supervisor.start_link(children, strategy: :one_for_one)
$ mix app.tree
plug
├── elixir
├── crypto
├── logger
│ └── elixir
└── mime
└── elixir
```
As the concurrency of your application goes up, the `ErrorReporter` process
might receive requests from many other processes and eventually become a
bottleneck. In a case like this, it could help to spin up multiple copies of the
`ErrorReporter` process under a `PartitionSupervisor`.
### mix escript.install
Mix also includes `mix escript.install` and `mix escript.uninstall` tasks for managing escripts. The tasks was designed in a way to mimic the existing `mix archive` functionality except that:
* Archives must be used sparingly because every new archive installed affects Mix performance, as every new archive is loaded when Mix boots. Escripts solve this by being managed apart from your Elixir/Mix installed
* Archives depends on the current Elixir version. Therefore, updating your Elixir version may break an archive. Fortunately, escripts include Elixir inside themselves, and therefore do not depend on your Elixir system version
Escripts will be installed at `~/.mix/escripts` which must be added to your [`PATH` environment variable](https://en.wikipedia.org/wiki/PATH_(variable)).
### Option parser integration
Elixir v1.3 includes improvements to the option parser, including `OptionParser.parse!/2` and `OptionParser.parse_head!/2` functions that will raise in case of invalid or unknown switches. Mix builds on top of this functionality to provide automatic error reporting solving a common complaint where invalid options were not reported by Mix tasks.
For example, invoking `mix test --unknown` in earlier Elixir versions would silently discard the `--unknown` option. Now `mix test` correctly reports such errors:
```
$ mix test --unknown
** (Mix) Could not invoke task "test": 1 error found!
--unknown : Unknown option
```
Note not all tasks have been updated to use strict option parsing. Some tasks, like `mix compile`, are actually a front-end to many other tasks, and as such, it cannot effectively assert which options are valid.
## ExUnit
ExUnit packs many improvements on the tooling side, better integration with external tools, as well as mechanisms to improve the readability of your tests.
### mix test --stale
ExUnit builds on top of `mix xref` to provide the `mix test --stale` functionality. When the `--stale` flag is given, `mix` will only run the tests that may have changed since the last time you ran `mix test --stale`. For example:
* If you saved a test file on disk, Mix will run that file and ignore the ones that have not changed
* If you changed a library file, for example, `lib/foo.ex` that defines `Foo`, any test that invokes a function in `Foo` directly or indirectly will also run
* If you modify your `mix.exs` or your `test/test_helper.exs`, Mix will run the whole test suite
This feature provides a great workflow for developers, allowing them to effortlessly focus on parts of the codebase when developing new features.
### Diffing
ExUnit will now include diff-ing output every time a developer asserts `assert left == right` in their tests. For example, the assertion:
```elixir
# Application supervisor
children = [
{PartitionSupervisor, child_spec: ErrorReporter, name: Reporters}
]
assert "fox jumps over the lazy dog" ==
"brown fox jumps over the dog"
```
The `PartitionSupervisor` will spin up a number of processes equal to
`System.schedulers_online()` by default (most often one per core). Now, when
routing requests to `ErrorReporter` processes we can use a `:via` tuple and
route the requests through the partition supervisor.
will fail with
```elixir
partitioning_key = self()
ErrorReporter.report({:via, PartitionSupervisor, {Reporters, partitioning_key}}, error)
1) test compare (Difference)
lib/ex_unit/examples/difference.exs:10
Assertion with == failed
lhs: "fox jumps over the lazy dog"
rhs: "brown fox jumps over the dog"
stacktrace:
lib/ex_unit/examples/difference.exs:11: (test)
```
Using `self()` as the partitioning key here means that the same process will
always report errors to the same `ErrorReporter` process, ensuring a form of
back-pressure. You can use any term as the partitioning key.
in a way that "lazy" in "lhs" will be shown in red to denote it has been removed from "rhs" while "brown" in "rhs" will be shown in green to denote it has been added to the "rhs".
### A Common Example
When working with large or nested data structures, the diffing algorithm makes it fast and convenient to spot the actual differences in the asserted values.
A common and practical example of a good use case for `PartitionSupervisor` is
partitioning something like a `DynamicSupervisor`. When starting many processes
under it, a dynamic supervisor can be a bottleneck, especially if said processes
take a long time to initialize. Instead of starting a single `DynamicSupervisor`,
you can start multiple:
### Test types
ExUnit v1.3 includes the ability to register different test types. This means libraries like QuickCheck can now provide functionality such as:
```elixir
children = [
{PartitionSupervisor, child_spec: DynamicSupervisor, name: MyApp.DynamicSupervisors}
]
defmodule StringTest do
use ExUnit.Case, async: true
use PropertyTestingLibrary
Supervisor.start_link(children, strategy: :one_for_one)
property "starts_with?" do
forall({s1, s2} <- {utf8, utf8}) do
String.starts_with?(s1 <> s2, s1)
end
end
end
```
Now you start processes on the dynamic supervisor for the right partition.
For instance, you can partition by PID, like in the previous example:
At the end of the run, ExUnit will also report it as a property, including both the amount of tests and properties:
```
1 property, 10 tests, 0 failures
```
### Named setups and describes
Finally, ExUnit v1.3 includes the ability to organize tests together in describe blocks:
```elixir
DynamicSupervisor.start_child(
{:via, PartitionSupervisor, {MyApp.DynamicSupervisors, self()}},
my_child_specification
)
defmodule StringTest do
use ExUnit.Case, async: true
describe "String.capitalize/2" do
test "uppercases the first grapheme" do
assert "T" <> _ = String.capitalize("test")
end
test "lowercases the remaining graphemes" do
assert "Test" = String.capitalize("TEST")
end
end
end
```
## Improved errors on binaries and evaluation
Every test inside a describe block will be tagged with the describe block name. This allows developers to run tests that belong to particular blocks, be them in the same file or across many files:
Erlang/OTP 25 improved errors on binary construction and evaluation. These improvements
apply to Elixir as well. Before v1.14, errors when constructing binaries would
often be hard-to-debug generic "argument errors". With Erlang/OTP 25 and Elixir v1.14,
more detail is provided for easier debugging. This work is part of [EEP
54](https://www.erlang.org/eeps/eep-0054).
```
$ mix test --only describe:"String.capitalize/2"
```
Before:
Note describe blocks cannot be nested. Instead of relying on hierarchy for composition, we want developers to build on top of named setups. For example:
```elixir
int = 1
bin = "foo"
int <> bin
#=> ** (ArgumentError) argument error
defmodule UserManagementTest do
use ExUnit.Case, async: true
describe "when user is logged in and is an admin" do
setup [:log_user_in, :set_type_to_admin]
test ...
end
describe "when user is logged in and is a manager" do
setup [:log_user_in, :set_type_to_manager]
test ...
end
defp log_user_in(context) do
# ...
end
end
```
Now:
By restricting hierarchies in favor of named setups, it is straight-forward for the developer to glance at each describe block and know exactly the setup steps involved.
```elixir
int = 1
bin = "foo"
int <> bin
#=> ** (ArgumentError) construction of binary failed:
#=> segment 1 of type 'binary':
#=> expected a binary but got: 1
```
## Slicing with steps
Elixir v1.12 introduced **stepped ranges**, which are ranges where you can
specify the "step":
```elixir
Enum.to_list(1..10//3)
#=> [1, 4, 7, 10]
```
Stepped ranges are particularly useful for numerical operations involving
vectors and matrices (see [Nx](https://github.com/elixir-nx/nx), for example).
However, the Elixir standard library was not making use of stepped ranges in its
APIs. Elixir v1.14 starts to take advantage of steps with support for stepped
ranges in a couple of functions. One of them is `Enum.slice/2`:
```elixir
letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
Enum.slice(letters, 0..5//2)
#=> ["a", "c", "e"]
```
`binary_slice/2` (and `binary_slice/3` for completeness) has been added to the
`Kernel` module, that works with bytes and also support stepped ranges:
```elixir
binary_slice("Elixir", 1..5//2)
#=> "lxr"
```
## Expression-based inspection and `Inspect` improvements
In Elixir, it's conventional to implement the `Inspect` protocol for opaque
structs so that they're inspected with a special notation, resembling this:
```elixir
MapSet.new([:apple, :banana])
#MapSet<[:apple, :banana]>
```
This is generally done when the struct content or part of it is private and the
`%name{...}` representation would reveal fields that are not part of the public
API.
The downside of the `#name<...>` convention is that *the inspected output is not
valid Elixir code*. For example, you cannot copy the inspected output and paste
it into an IEx session.
Elixir v1.14 changes the convention for some of the standard-library structs.
The `Inspect` implementation for those structs now returns a string with a valid
Elixir expression that recreates the struct when evaluated. In the `MapSet`
example above, this is what we have now:
```elixir
fruits = MapSet.new([:apple, :banana])
MapSet.put(fruits, :pear)
#=> MapSet.new([:apple, :banana, :pear])
```
The `MapSet.new/1` expression evaluates to exactly the struct that we're
inspecting. This allows us to hide the internals of `MapSet`, while keeping
it as valid Elixir code. This expression-based inspection has been
implemented for `Version.Requirement`, `MapSet`, and `Date.Range`.
Finally, we have improved the `Inspect` protocol for structs so that
fields are inspected in the order they are declared in `defstruct`.
The option `:optional` has also been added when deriving the `Inspect`
protocol, giving developers more control over the struct representation.
See the updated documentation for `Inspect` for a general rundown on
the approaches and options available.
## v1.14.5 (2023-05-22)
This release contains fixes for Erlang/OTP 26.
### Bug fixes
#### Elixir
* [CLI] Fix a bug where stdout would block when there was no attached terminal on Windows when running on Erlang/OTP 26
#### Mix
* [Mix] Properly set SSL configuration for Mix downloads when running on Erlang/OTP 26
## v1.14.4 (2023-04-03)
This release adds basic support for Erlang/OTP 26. When migrating
to Erlang/OTP 26, keep it mind it changes how maps are stored
internally and they will be printed and traversed in a different
order (note maps never provided a guarantee of their order).
To aid migration, this release adds `:sort_maps` to `inspect`
custom options, in case you want to sort them before inspection:
inspect(map, custom_options: [sort_maps: true])
### Enhancements
#### Elixir
* [Inspect] Add `:sort_maps` to `Inspect.Opts.custom_options`
#### IEx
* [IEx] Support shell history in Erlang/OTP 26+
#### Mix
* [mix compile.elixir] Optimize application tracer
### Bug fixes
#### Elixir
* [Code] Properly handle blocks with comments in all cases in `Code.quoted_to_string_with_comments/2`
* [Kernel] Fix `debug_info/4` when returning core_v1
* [Kernel] Store complete path on `quote keep: true` to avoid invalid stacktraces
* [Kernel] Fix column count when tokenizing escaped interpolations
* [Stream] Fix `Stream.zip/1` hanging on empty list
#### Mix
* [mix format] Don't call formatter on directories
## v1.14.3 (2023-01-14)
## v1.3.2 (2016-07-15)
### 1. Enhancements
#### Elixir
* [Kernel] Speed up loading of runtime modules in the parallel compiler
* [Range] Optimize range membership implementation
* [Kernel] Support guards in `else` clauses in `with`
#### ExUnit
#### Mix
* [ExUnit] Return values from running doctests and make their order consistent
* [Mix] Add `MIX_NO_DEPS` env var for disabling dep loading. Used for third-party scripts and tools like Nix package manager
* [Mix] Add `mix test --listen-on-stdin` that automatically reruns tests on stdin
* [Mix] Disable `--warnings-as-errors` when compiling dependencies
* [Mix] Add `--filter` option to `mix deps.unlock` to unlock only matching dependencies
### 2. Bug fixes
#### Elixir
* [Calendar] Fix handling of negative years in `Calendar.strftime/2`
* [DateTime] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `DateTime.add/3`)
* [Exception] Improve blaming of FunctionClauseError with `is_struct/2` guards
* [Kernel] Fix invalid variable scoping in `defguard` expansion
* [Kernel] Do not warn on captured underscored vars from `defmodule`
* [Kernel] Do not crash for missing line info on type warnings
* [NaiveDateTime] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `NaiveDateTime.add/3`)
* [Macro] Fix `Macro.to_string/1` for large negative integers
* [Macro] Properly type and escape expansion of `__ENV__` in macros
* [Path] Make sure `Path.wildcard/2` expands `..` symlinks accordingly
* [Range] Address corner cases in `Range.disjoint?/2` implementation
* [Time] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `Time.add/3`)
#### ExUnit
* [ExUnit.DocTest] Remove unnecessary literal quotes from error message on reports
## v1.14.2 (2022-11-11)
### 1. Enhancements
#### Elixir
* [Code] Add `Code.eval_quoted_with_env/4` with support for the `:prune_binding` option
#### ExUnit
* [ExUnit.Case] Allow test cases to not be registered on use
* [ExUnit.DocTest] Include `:doctest` and `:doctest_line` as meta tags
* [ExUnit.Formatter] Expose `ExUnit.Formatter.format_assertion_diff/4`
* [Enum] Return `nil` if enumerable halts in `Enum.find_index/3`
* [Kernel] Do not attempt to load modules that have not been required when invoking regular functions, otherwise this invalidates the `@compile {:autoload, false}` directive.
#### Mix
* [Mix] `Mix.install/2` accepts atoms as paths
* [Mix] Ensure missing protocol dependencies are discarded in umbrella projects with shared build
#### ExUnit
* [ExUnit.Diff] Ensure no leading or trailing when diffing some maps
## v1.3.1 (2016-06-28)
### 1. Enhancements
#### IEx
* [IEx.Helpers] `Add import_file_if_available` for importing files only if they are available
* [IEx.Helpers] `Add import_if_available` for importing modules only if they are available
### 2. Bug fixes
#### Elixir
* [Code.Formatter] Fix `size*unit` shortcut in bitstring
* [Kernel] Generate unique variables for macro expansion of `defguard`
* [Protocol] Expand `:for` in protocols with the appropriate env
#### ExUnit
* [ExUnit] Do not run duplicate cases on `ExUnit.run/1`
#### Mix
* [mix test] Ensure proper error message when there is no test directory
## v1.14.1 (2022-10-10)
### 1. Enhancements
#### Elixir
* [Kernel] Perform partial expansion of literals in module attributes
* [Kernel] Do not add compile-time dependencies for literals as defaults in `Application.compile_env/3` inside module attributes
* [Macro] Add `Macro.expand_literals/2` and `Macro.expand_literals/3`
* [System] Add `:close_stdin` to `System.shell/2`
#### Mix
* [mix test] Accept `--all-warnings` option
## 2. Bug fixes
#### Elixir
* [Kernel] Fix misleading warning when `:uniq` is given in `for` comprehensions and the result is unused
* [Kernel] Improve error message for when there is a conflicting struct and ignoring module conflict
* [Kernel] Do not delete `@enforce_keys` attribute after `defstruct` declaration
* [Kernel] Do not crash the checker on modules with missing `:debug_info` chunk
* [Macro] Fix error in `Macro.to_string/2` when converting an AST with `:erlang.binary_to_atom/2`
* [String] Fix `String.split/3` and `String.next_grapheme/1` returning invalid results on invalid UTF-8 encoding
* [System] Do not close stdin by default in `System.shell/2`
* [URI] Do not return `uri.port` as `:undefined` in certain cases in `URI.new/1`
#### ExUnit
* [ExUnit.DocTest] Do not crash when both `:moduledoc` and functions are specified in `:only`
* [Kernel] Ensure structs can be expanded in dynamic module names
* [Kernel] Ensure aliases warnings are not accidentally discarded when the same module is imported
* [Kernel.ParallelCompiler] Ensure two modules with cyclic struct dependencies cannot run into a deadlock when compiling
* [Kernel.Typespec] Support module attributes in remote types
* [Module] Do not expect stacktraces to be always present when dispatching to locals during the module compilation
#### IEx
* [CLI] Fix invalid argument handling when `--no-pry` is given
* [IEx.Helpers] Fix `h` helper for operators
#### Mix
* [mix format] Do not cache inputs from `.formatter.exs` so they are properly re-evaluted on every call
* [Mix] Do not load modules for xref purposes, instead use BEAM info
* [Mix] Ensure `deps.check` does not check archives (that's done in loadpaths)
* [Mix] Validate application properties before traversing them
* [Mix] Check for proper Makefile when compiling on Windows
* [Mix] Enforce space after comma in `mix do`
## v1.14.0 (2022-09-01)
## v1.3.0 (2016-06-21)
### 1. Enhancements
#### EEx
* [EEx] Support multi-line comments to EEx via `<%!-- --%>`
* [EEx] Add `EEx.tokenize/2`
* [EEx.Engine] Support an `init/1` function in engines that will return the initial buffer (defaults to an empty string)
#### Elixir
* [Access] Add `Access.slice/1`
* [Application] Add `Application.compile_env/4` and `Application.compile_env!/3` to read the compile-time environment inside macros
* [Calendar] Support ISO8601 basic format parsing with `DateTime.from_iso8601/2`
* [Calendar] Add `day`/`hour`/`minute` on `add`/`diff` across different calendar modules
* [Code] Add `:normalize_bitstring_modifiers` to `Code.format_string!/2`
* [Code] Emit deprecation and type warnings for invalid options in on `Code.compile_string/2` and `Code.compile_quoted/2`
* [Code] Warn if an outdated lexical tracker is given on eval
* [Code] Add `Code.env_for_eval/1` and `Code.eval_quoted_with_env/3`
* [Code] Improve stacktraces from eval operations on Erlang/OTP 25+
* [Code.Fragment] Add support for `__MODULE__` in several functions
* [Code.Fragment] Support surround and context suggestions across multiple lines
* [Enum] Allow slicing with steps in `Enum.slice/2`
* [File] Support `dereference_symlinks: true` in `File.cp/3` and `File.cp_r/3`
* [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero
* [Float] Add `Float.min_finite/0` and `Float.max_finite/0`
* [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol
* [Inspect] Allow `:optional` when deriving the Inspect protocol for hiding fields that match their default value
* [Inspect] Inspect struct fields in the order they are declared in `defstruct`
* [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement`
* [IO] Support `Macro.Env` and keywords as stacktrace definitions in `IO.warn/2`
* [IO] Add `IO.ANSI.syntax_colors/0` and related configuration to be shared across IEx and `dbg`
* [Kernel] Add new `dbg/0-2` macro
* [Kernel] Allow any guard expression as the size of a bitstring in a pattern match
* [Kernel] Allow composite types with pins as the map key in a pattern match
* [Kernel] Print escaped version of control chars when they show up as unexpected tokens
* [Kernel] Warn on confusable non-ASCII identifiers
* [Kernel] Add `..` as a nullary operator that returns `0..-1//1`
* [Kernel] Implement Unicode Technical Standard #39 recommendations. In particular, we warn for confusable scripts and restrict identifiers to single-scripts or highly restrictive mixed-scripts
* [Kernel] Automatically perform NFC conversion of identifiers
* [Kernel] Add `binary_slice/2` and `binary_slice/3`
* [Kernel] Lazily expand module attributes to avoid compile-time deps
* [Kernel] Automatically cascade `generated: true` annotations on macro expansion
* [Keyword] Add `Keyword.from_keys/2` and `Keyword.replace_lazy/3`
* [List] Add `List.keysort/3` with support for a `sorter` function
* [Macro] Add `Macro.classify_atom/1` and `Macro.inspect_atom/2`
* [Macro] Add `Macro.path/2`
* [Macro.Env] Add `Macro.Env.prune_compile_info/1`
* [Map] Add `Map.from_keys/2` and `Map.replace_lazy/3`
* [MapSet] Add `MapSet.filter/2`, `MapSet.reject/2`, and `MapSet.symmetric_difference/2`
* [Node] Add `Node.spawn_monitor/2` and `Node.spawn_monitor/4`
* [Module] Support new `@after_verify` attribute for executing code whenever a module is verified
* [PartitionSupervisor] Add `PartitionSupervisor` that starts multiple isolated partitions of the same child for scalability
* [Path] Add `Path.safe_relative/1` and `Path.safe_relative_to/2`
* [Registry] Add `Registry.count_select/2`
* [Stream] Add `Stream.duplicate/2` and `Stream.transform/5`
* [String] Support empty lookup lists in `String.replace/3`, `String.split/3`, and `String.splitter/3`
* [String] Allow slicing with steps in `String.slice/2`
* [Task] Add `:zip_input_on_exit` option to `Task.async_stream/3`
* [Task] Store `:mfa` in the `Task` struct for reflection purposes
* [URI] Add `URI.append_query/2`
* [Version] Add `Version.to_string/1`
* [Version] Colorize `Version.Requirement` source in the `Inspect` protocol
* [Access] Add support for `Access.all/0`, `Access.elem/1`, `Access.key/2` and `Access.key!/1` for traversing nested data structures
* [Calendar] Add `Calendar` and `Date`, `Time`, `NaiveDateTime` and `DateTime` types
* [CLI] Add `--logger-otp-reports BOOL` and `--logger-sasl-reports BOOL` switches
* [Compiler] Emit a summary of compilation errors when modules are missing
* [Enum] Add `Enum.group_by/3` that allows developers to map on the value being grouped
* [Enum] Make list values in maps returned by `Enum.group_by/2` and `Enum.group_by/3` preserve the order of the input enumerable instead of reversing it.
* [Enum] Add `Enum.drop_every/2` that drops every `nth`, including the first one
* [Exception] Suggest possible functions on `UndefinedFunctionError` for existing modules
* [Exception] Warn if unknown fields are given to `raise/2`
* [File] Support IO devices in `File.copy/3`
* [GenServer] Raise a more meaningful exit if you try to `GenServer.call/3` yourself
* [Inspect] Support `:base` option when inspecting binaries
* [IO] Add `IO.warn/2` that will print a warning message with stacktrace and notify the compiler a warning was printed (in case --warnings-as-errors was enabled)
* [Kernel] Support `generated: true` in quote
* [Kernel] Support `Kernel.pop_in/1` and `Kernel.pop_in/2` for yanking a value from a nested data structure
* [Kernel] Allow variable struct names when matching, for example, `%module{key: "value"} = struct`
* [Kernel] Allow guards on the left side of `<-` in `for` and `with` special forms
* [Kernel] Support `else` chunks in `with`
* [Kernel] Track `{module, function, arity}` imports and warn on unused ones when such are specified in `:only`
* [Kernel] Add `keyword/0` and `keyword/1` built-in types to typespecs
* [Kernel] Add sigils for date (`~D[2015-04-17]`), time (`~T[08:00:00]`) and naive date times `~N[2015-04-17 08:00:00]`
* [Kernel] Support `@enforce_keys` on `defstruct/1` to guarantee some keys are explicitly given when building structs
* [OptionParser] Add support for `:count` switch type
* [OptionParser] Add `parse!/2` and `parse_head!/2` that raise `OptionParser.ParseError` in case of errors
* [Process] Add `Process.sleep/1`
* [Range] `Range.range?/1` now checks the validity of a range.
* [Regex] Support `:include_captures` in `Regex.split/3`
* [String] Add `String.myers_difference/2` for calculating the difference between two strings
* [System] Add `System.os_time/0` and `System.os_time/1`
* [Typespec] Add support for `%{required(foo) => bar}` and `%{optional(foo) => bar}` forms (Erlang 19 only)
* [Typespec] Add support for `@optional_callbacks` to mark certain that certain callbacks may be optionally implemented
* [Typespec] Introduce `%{...}` to mean any map (Erlang 19 only)
* [URI] Add `URI.merge/2`
* [Version] Add `Version.parse!/1`
#### ExUnit
* [ExUnit] Add `ExUnit.Callbacks.start_link_supervised!/2`
* [ExUnit] Add `ExUnit.run/1` to rerun test modules
* [ExUnit] Colorize summary in yellow with message when all tests are excluded
* [ExUnit] Display friendly error when test name is too long
* [ExUnit] Show pinned variables on failed `assert ^left = right` and `assert match?(^left, right)` assertions
* [ExUnit] Add `ExUnit.Case.register_attribute` which allow attributes to be cleaned up whenever a test is defined
* [ExUnit] Add `ExUnit.Case.register_test` and support the ability to tag "tests" by type. This will allow projects like QuickCheck to change the wording in formatters to say "10 properties" instead of "10 tests"
* [ExUnit] Support diffing of values when using `==` in `assert`
* [ExUnit] Start running tests as soon as cases are loaded. This feature is enabled by default when running tests through Mix
* [ExUnit] Raise a straight-forward error message in case a duplicate test name is defined
* [ExUnit] Bump the default number of max cases to double of schedulers to support both IO and CPU bound tests
* [ExUnit] Support for named setups in `setup` and `setup_all`
* [ExUnit] Support for bundling tests together with `describe/2`
#### IEx
* [IEx] Evaluate `--dot-iex` line by line
* [IEx] Add line-by-line evaluation of IEx breakpoints
* [IEx.Autocomplete] Autocomplete bitstrings modifiers (after `::` inside `<<...>>`)
* [IEx.Helpers] Allow an atom to be given to `pid/1`
* [IEx.Helpers] Support sigils in `h/1`
* [IEx] Add `nl/2` that loads a given module on a list of nodes
* [IEx.Helpers] No longer restart applications on `recompile/1`
* [IEx.Autocomplete] Improve IEx expand to handle functions after `&`
#### Logger
* [Logger] Add `Logger.put_process_level/2`
* [Logger] Introduce `Logger.reset_metadata/0,1`
#### Mix
* [Mix] Add `:config_path` and `:lockfile` options to `Mix.install/2`
* [mix compile] Add `--no-optional-deps` to skip optional dependencies to test compilation works without optional dependencies
* [mix compile] Include column information on error diagnostics when possible
* [mix deps] `Mix.Dep.Converger` now tells which deps formed a cycle
* [mix do] Support `--app` option to restrict recursive tasks in umbrella projects
* [mix do] Allow using `+` as a task separator instead of comma
* [mix format] Support filename in `mix format -` when reading from stdin
* [mix format] Compile if `mix format` plugins are missing
* [mix new] Do not allow projects to be created with application names that conflict with multi-arg Erlang VM switches
* [mix profile] Return the return value of the profiled function
* [mix release] Make BEAM compression opt-in
* [mix release] Let `:runtime_config_path` accept `false` to skip the `config/runtime.exs`
* [mix test] Improve error message when suite fails due to coverage
* [mix test] Support `:test_elixirc_options` and default to not generating docs nor debug info chunk for tests
* [mix xref] Support `--group` flag in `mix xref graph`
* [Mix] Add `mix xref` and `mix compile.xref` that runs cross-reference checks, with the latter running after compilation by default
* [Mix] Add `mix app.tree` and `mix deps.tree`
* [Mix] Add `Mix.Task.rerun/2` that reenables and re-runs a task
* [Mix] Integrate `OptionParser.ParseError` into Mix, automatically converting such exceptions into `Mix.Error` and embedding the task information
* [Mix] Support `@preferred_cli_env` attribute when defining tasks
* [Mix] Support `mix test --raise` that will raise when a test suite fails (instead of setting the exit code to 1)
* [Mix] Enable rebar3 manager by default for Hex dependencies
* [Mix] Add `mix escript.install` to install escripts
* [Mix] Print stacktraces for `Mix.Error` when `MIX_DEBUG=1` is set
* [Mix] Add a user friendly error for merge conflicts on `mix.lock`
* [Mix] Track files between path dependencies. This means umbrella applications will no longer trigger full recompilation when a sibling changes. Instead it will only recompile the files affected by the sibling changes
* [Mix] No longer print every file being compiled. Instead a generic "Compiling N files (.ext)" will be printed and files will only be logged in case they take more than 5 seconds to compile. This threshold can be customized by passing the `--long-compilation-threshold` flag and the previous behaviour can be reenabled by giving `--verbose` to `mix compile`
* [Mix] Add `mix test --stale` that uses static analysis on source files to know which tests should run when source files changes. If any test file changes, it will also re-run. Changing a configuration file or the test helper will trigger a full recompilation
### 2. Bug fixes
#### Elixir
* [Calendar] Handle widths with "0" in them in `Calendar.strftime/3`
* [CLI] Improve errors on incorrect `--rpc-eval` usage
* [CLI] Return proper exit code on Windows
* [Code] Do not emit warnings when formatting code
* [Enum] Allow slices to overflow on both starting and ending positions
* [Kernel] Do not allow restricted characters in identifiers according to UTS39
* [Kernel] Define `__exception__` field as `true` when expanding exceptions in typespecs
* [Kernel] Warn if any of `True`, `False`, and `Nil` aliases are used
* [Kernel] Warn on underived `@derive` attributes
* [Kernel] Remove compile-time dependency from `defimpl :for`
* [Kernel] Track all arities on imported functions
* [Kernel] Fix equality in guards for dynamic ranges without steps
* [Module] Fix loop while unifying type variables
* [Protocol] Warn if a protocol has no definitions
* [Regex] Show list options when inspecting a Regex manually defined with `Regex.compile/2`
* [String] Allow slices to overflow on both starting and ending positions
* [System] Raise non-generic exception on missing env in `System.fetch_env!/1` to mirror map operations
#### ExUnit
* [ExUnit] Do not crash when diffing unknown bindings in guards
* [ExUnit] Properly print diffs when comparing improper lists with strings at the tail position
* [ExUnit] Add short hash to `tmp_dir` in ExUnit to avoid test name collision
* [ExUnit] Do not store logs in the CLI formatter (this reduces memory usage for suites with `capture_log`)
* [ExUnit] Run `ExUnit.after_suite/1` callback even when no tests run
* [ExUnit] Fix scenario where `setup` with imported function from within `describe` failed to compile
#### IEx
* [IEx] Disallow short-hand pipe after matches
* [IEx] Fix `exports/1` in IEx for long function names
* [Application] Ensure `Application.spec/2` returns nil for unknown applications
* [GenServer] Ensures `cast/2` returns `:ok` if locally registered process is not found
* [Inspect] Ensure binaries break into new lines when inspected
* [Kernel] Do not choke on capture operator with argument above `&191`
* [Kernel] Raise if `defstruct` is called multiple times
* [Kernel] Ensure `Module.create/3` respects var/alias hygiene
* [Kernel] Support non-literal ranges on the right side of `in/2`
* [Macro] Fix `Macro.to_string/1` on a call of a capture argument, for example `&(&1).(:x)`
* [OptionParser] Allow `OptionParser` to parse negative numbers
* [Record] Fix `Record.is_record/2` when dealing with non-record tuples
* [String] Ensure `strip` also removes non-breaking whitespaces (and ensure `split` still does not split on them)
* [URI] Use square brackets for IPv6 in `URI.to_string/1`
#### Mix
* [mix compile.elixir] Fix `--warnings-as-errors` when used with `--all-warnings`
* [mix compile.elixir] Ensure semantic recompilation cascades to path dependencies
* [mix compile.elixir] Lock the compiler to avoid concurrent usage
* [mix format] Do not add new lines if the formatted file is empty
* [mix format] Properly compile dependencies on `mix format`
* [mix release] Only set `RELEASE_MODE` after `env.{sh,bat}` are executed
* [mix release] Allow application mode configuration to cascade to dependencies
* [mix xref] Do not emit already consolidated warnings during `mix xref trace`
* [Mix] Do not start apps with `runtime: false` on `Mix.install/2`
* [Mix] Improve task not found message when Mix would include the not found task as a suggestion due to different casing
* [Mix] Ignore lock revision when the lock is out of date when updating Mix dependencies. Before this fix, Git tags and branches in the lock file would erroneously take higher precedence than the one in `mix.exs`
* [Mix] Only recompile empty Elixir files if they change instead of recompiling them on every run
* [Mix] Ensure .app file is written in UTF-8 (this allows app descriptions to contain UTF-8 characters)
* [Mix.Dep] Always specify the `:env` option internally for dependencies to avoid false positives in the dependency resolution
* [Mix.Dep] Correctly detect conflict from cousin optional dependencies in the dependency resolution algorithm
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Float] `Float.to_string/2` and `Float.to_char_list/2` has been soft-deprecated as Elixir will now attempt to print the shortest and most accurate representation by default. Developers can always fallback to `:erlang.float_to_binary/2` and `:erlang.float_to_list/2` if they need the previous functionality
* [Kernel] `to_char_list` functions have been soft-deprecated in favor of `to_charlist`. This aligns with the naming conventions in both Erlang and Elixir
* [String] The confusing `String.strip/2`, `String.lstrip/2` and `String.rstrip/2` API has been soft deprecated in favor of `String.trim/2`, `String.trim_leading/2` and `String.trim_trailing/2`
* [String] The confusing `String.ljust/3` and `String.rjust/3` API has been soft deprecated in favor of `String.pad_leading/3` and `String.pad_trailing/3`
* [Typespec] `char_list` is soft-deprecated in favor of `charlist`
* [File] Passing a callback as third argument to `File.cp/3` and `File.cp_r/3` is deprecated.
Instead pass the callback the `:on_conflict` key of a keyword list
### 4. Deprecations
#### EEx
* [EEx] Using `<%# ... %>` for comments is deprecated. Please use `<% # ... %>` or the new multi-line comments with `<%!-- ... --%>`
#### Logger
* [Logger] Deprecate `Logger.enable/1` and `Logger.disable/1` in favor of `Logger.put_process_level/2`
#### Mix
* [mix cmd] The `--app` option in `mix cmd CMD` is deprecated in favor of the more efficient `mix do --app app cmd CMD`
### 4. Hard deprecations
This release deprecates many APIs that have been soft-deprecated in previous Elixir versions.
#### Elixir
* [Application] Calling `Application.get_env/3` and friends in the module body is now discouraged, use `Application.compile_env/3` instead
* [Bitwise] `use Bitwise` is deprecated, use `import Bitwise` instead
* [Bitwise] `~~~` is deprecated in favor of `bnot` for clarity
* [Kernel.ParallelCompiler] Returning a list or two-element tuple from `:each_cycle` is deprecated, return a `{:compile | :runtime, modules, warnings}` tuple instead
* [Kernel] Deprecate the operator `<|>` to avoid ambiguity with upcoming extended numerical operators
* [String] Deprecate passing a binary compiled pattern to `String.starts_with?/2`
#### Logger
* [Logger] Deprecate `$levelpad` on message formatting
#### Mix
* [Mix] `Mix.Tasks.Xref.calls/1` is deprecated in favor of compilation tracers
### 5. Backwards incompatible changes
#### Mix
* [mix local.rebar] Remove support for rebar2, which has not been updated in 5 years, and is no longer supported on recent Erlang/OTP versions
## v1.13
The CHANGELOG for v1.13 releases can be found [in the v1.13 branch](https://github.com/elixir-lang/elixir/blob/v1.13/CHANGELOG.md).
* [Dict] `Dict` is no longer a behaviour and its functions will be deprecated in upcoming releases
* [Enum] Passing a dictionary to `Enum.group_by/3` is deprecated
* [Kernel] `\x{H*}` in strings/sigils/charlists is deprecated
* [Kernel] Add deprecation for `defdelegate` list arguments and `:append_first` option. The previously undocumented and deprecated support for matching has been removed
* [Kernel] Warn if a variable is assigned inside `case`/`if`/etc and used outside the block
* [Keyword] `Keyword.size/1` is deprecated in favor of `Kernel.length/1`
* [Map] `Map.size/1` is deprecated in favor of `Kernel.map_size/1`
* [Regex] The option `/r` (for ungreedy) has been deprecated in favor of `/U`
* [Set] `Set` is no longer a behaviour and its functions will be deprecated in upcoming releases
* [String] `String.valid_character?/1` is deprecated in favor of `String.valid?/1` with pattern matching
* [Task] `Task.find/2` is deprecated in favor of explicit message matching
* [URI] Passing a non-map to `URI.decode_query/2` is deprecated
+10 -20
View File
@@ -13,23 +13,17 @@ The goal of the Code of Conduct is to specify a baseline standard of behavior so
These are the values Elixir developers should aspire to:
* Be friendly and welcoming
* Be kind
* Be patient
* Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.)
* Interpret the arguments of others in good faith, do not seek to disagree.
* When we do disagree, try to understand why.
* Be thoughtful
* Productive communication requires effort. Think about how your words will be interpreted.
* Remember that sometimes it is best to refrain entirely from commenting.
* Be respectful
* In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively.
* Be constructive
* Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
* Avoid unconstructive criticism: don't merely decry the current state of affairs; offer — or at least solicit — suggestions as to how things may be improved.
* Avoid harsh words and stern tone: we are all aligned towards the well-being of the community and the progress of the ecosystem. Harsh words exclude, demotivate, and lead to unnecessary conflict.
* Avoid snarking (pithy, unproductive, sniping comments).
* Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults towards a project, person or group).
* Be responsible
* What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise.
* Avoid destructive behavior
* Derailing: stay on topic; if you want to talk about something else, start a new conversation.
* Unconstructive criticism: don't merely decry the current state of affairs; offer (or at least solicit) suggestions as to how things may be improved.
* Snarking (pithy, unproductive, sniping comments).
The following actions are explicitly forbidden:
@@ -45,13 +39,13 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
* The [official GitHub projects][1] and code reviews.
* The official GitHub projects and code reviews.
* The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
* The #elixir-lang IRC channel on Freenode.
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
Other Elixir activities (such as conferences, meetups, and other unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Project maintainers may remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
@@ -59,8 +53,4 @@ Instances of abusive, harassing, or otherwise unacceptable behavior may be repor
## Acknowledgements
This document was based on the Code of Conduct from the Go project (dated Sep/2021) and the Contributor Covenant (v1.4).
[1]: https://github.com/elixir-lang/
[2]: https://web.libera.chat/#elixir
[3]: https://libera.chat/
This document was based on the Code of Conduct from the Go project with parts derived from Django's Code of Conduct, Rust's Code of Conduct and the Contributor Covenant.
+18
View File
@@ -0,0 +1,18 @@
### Precheck
* Do not use the issues tracker for help or support (try Stack Overflow, IRC, mailing list, etc)
* For proposing a new feature, please start a discussion on the Elixir Core mailing list
* For bugs, do a quick search and make sure the bug has not yet been reported
* Finally, be nice and have fun!
### Environment
* Elixir version (elixir -v):
* Operating system:
### Current behavior
Include code samples, errors and stacktraces if appropriate.
### Expected behavior
+10 -173
View File
@@ -1,176 +1,13 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright 2012 Plataformatec
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
1. Definitions.
http://www.apache.org/licenses/LICENSE-2.0
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+92 -158
View File
@@ -1,46 +1,42 @@
REBAR ?= "$(CURDIR)/rebar"
PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := 1.14/
CANONICAL ?= main/
DOCS_FORMAT ?= html
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
DOCS := v1.3
CANONICAL := stable
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
ERLC := erlc -I lib/elixir/include
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
Q := @
LIBDIR := lib
BINDIR := bin
INSTALL = install
INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
.PHONY: install compile erlang elixir build_plt clean_plt dialyze test clean install_man clean_man docs Docs.zip Precompiled.zip publish_zips publish_docs publish_mix
.NOTPARALLEL: compile
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 23)])' -s erlang halt | grep -q '^true'; \
$(Q) erl -noshell -eval 'io:fwrite("~s", [erlang:system_info(otp_release) >= "18"])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 23.0 is required to build Elixir"; \
exit 1; \
fi
echo "At least Erlang 18.0 is required to build Elixir"; \
exit 1; \
fi;
endef
define APP_TEMPLATE
$(1): lib/$(1)/ebin/Elixir.$(2).beam lib/$(1)/ebin/$(1).app
lib/$(1)/ebin/$(1).app: lib/$(1)/mix.exs
$(Q) cd lib/$(1) && ../../bin/elixir -e 'Mix.start(:permanent, [])' -r mix.exs -e 'Mix.Task.run("compile.app", ~w[--compile-path ebin])'
$(Q) mkdir -p lib/$(1)/_build/shared/lib/$(1)
$(Q) cp -R lib/$(1)/ebin lib/$(1)/_build/shared/lib/$(1)/
$(Q) cd lib/$(1) && ../../bin/elixir -e 'Mix.start(:permanent, [])' -r mix.exs -e 'Mix.Task.run("compile.app")'
$(Q) cp lib/$(1)/_build/shared/lib/$(1)/ebin/$(1).app lib/$(1)/ebin/$(1).app
$(Q) rm -rf lib/$(1)/_build
lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1)/lib/*/*.ex) $(wildcard lib/$(1)/lib/*/*/*.ex)
@ echo "==> $(1) (compile)"
@@ -48,66 +44,51 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
test_$(1): compile $(1)
@ echo "==> $(1) (ex_unit)"
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
endef
define WRITE_SOURCE_DATE_EPOCH
$(shell mkdir -p $(SOURCE_DATE_EPOCH_PATH) && bin/elixir -e \
'IO.puts System.build_info()[:date] \
|> DateTime.from_iso8601() \
|> elem(1) \
|> DateTime.to_unix()' > $(SOURCE_DATE_EPOCH_FILE))
endef
define READ_SOURCE_DATE_EPOCH
$(strip $(shell cat $(SOURCE_DATE_EPOCH_FILE)))
@ echo "==> $(1) (exunit)"
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/*_test.exs";
endef
#==> Compilation tasks
APP := lib/elixir/ebin/elixir.app
PARSER := lib/elixir/src/elixir_parser.erl
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
KERNEL:=lib/elixir/ebin/Elixir.Kernel.beam
UNICODE:=lib/elixir/ebin/Elixir.String.Unicode.beam
default: compile
compile: erlang $(APP) elixir
compile: lib/elixir/src/elixir.app.src erlang elixir
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
lib/elixir/src/elixir.app.src: src/elixir.app.src
$(Q) $(call CHECK_ERLANG_RELEASE)
$(Q) rm -rf lib/elixir/src/elixir.app.src
$(Q) echo "%% This file is automatically generated from <project_root>/src/elixir.app.src" \
>lib/elixir/src/elixir.app.src
$(Q) cat src/elixir.app.src >>lib/elixir/src/elixir.app.src
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
erlang:
$(Q) cd lib/elixir && $(REBAR) compile
# Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file,
# then Mix, and then compile EEx fully
# Since Mix depends on EEx and EEx depends on
# Mix, we first compile EEx without the .app
# file, then mix and then compile EEx fully
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler core -s erlang halt; \
fi
$(Q) "$(MAKE)" unicode
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/kernel.ex" -o ebin;
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
$(Q) "$(MAKE)" app
app: $(APP)
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
$(Q) $(MAKE) unicode
$(Q) rm -rf lib/elixir/ebin/elixir.app
$(Q) cd lib/elixir && $(REBAR) compile
unicode: $(UNICODE)
$(UNICODE): lib/elixir/unicode/*
@ echo "==> unicode (compile)";
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(Q) cd lib/elixir && ../../$(ELIXIRC) unicode/unicode.ex -o ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -118,148 +99,103 @@ $(eval $(call APP_TEMPLATE,iex,IEx))
install: compile
@ echo "==> elixir (install)"
$(Q) for dir in lib/*; do \
rm -rf $(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin; \
rm -Rf $(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin; \
$(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
$(INSTALL_DATA) $$dir/ebin/* "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
done
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
$(Q) $(INSTALL_PROGRAM) $(filter-out %.ps1, $(filter-out %.bat, $(wildcard bin/*))) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(BINDIR)"
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/*; do \
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/$(BINDIR)/"; \
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/bin"
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/* ; do \
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/bin/" ; \
done
"$(MAKE)" install_man
check_reproducible: compile
$(Q) echo "==> Checking for reproducible builds..."
$(Q) rm -rf lib/*/tmp/ebin_reproducible/
$(call WRITE_SOURCE_DATE_EPOCH)
$(Q) mkdir -p lib/elixir/tmp/ebin_reproducible/ \
lib/eex/tmp/ebin_reproducible/ \
lib/ex_unit/tmp/ebin_reproducible/ \
lib/iex/tmp/ebin_reproducible/ \
lib/logger/tmp/ebin_reproducible/ \
lib/mix/tmp/ebin_reproducible/
$(Q) mv lib/elixir/ebin/* lib/elixir/tmp/ebin_reproducible/
$(Q) mv lib/eex/ebin/* lib/eex/tmp/ebin_reproducible/
$(Q) mv lib/ex_unit/ebin/* lib/ex_unit/tmp/ebin_reproducible/
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
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) echo "Builds are reproducible"
$(MAKE) install_man
clean:
cd lib/elixir && $(REBAR) clean
rm -rf ebin
rm -rf lib/*/ebin
rm -rf $(PARSER)
$(Q) "$(MAKE)" clean_residual_files
rm -rf lib/elixir/test/ebin
rm -rf lib/*/tmp
rm -rf lib/mix/test/fixtures/git_repo
rm -rf lib/mix/test/fixtures/deps_on_git_repo
rm -rf lib/mix/test/fixtures/git_rebar
rm -rf lib/elixir/src/elixir.app.src
$(MAKE) clean_man
clean_elixir:
clean_exbeam:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/
rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/
rm -rf lib/mix/test/fixtures/deps_on_git_repo/
rm -rf lib/mix/test/fixtures/git_rebar/
rm -rf lib/mix/test/fixtures/git_repo/
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump
$(Q) "$(MAKE)" clean_man
#==> Documentation tasks
#==> Create Documentation
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
COMPILE_DOCS = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}\c")
COMPILE_DOCS = bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" -m "$(3)" -u "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) -o doc/$(2) -a http://elixir-lang.org/docs/$(CANONICAL)/$(2)/ -p http://elixir-lang.org/docs.html $(4)
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
docs_elixir: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (elixir)"
$(Q) rm -rf doc/elixir
$(call COMPILE_DOCS,Elixir,elixir,Kernel,--config "lib/elixir/docs.exs")
$(call COMPILE_DOCS,Elixir,elixir,Kernel,-e "lib/elixir/pages/Behaviours.md" -e "lib/elixir/pages/Naming Conventions.md" -e "lib/elixir/pages/Typespecs.md" -e "lib/elixir/pages/Writing Documentation.md")
docs_eex: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (eex)"
$(Q) rm -rf doc/eex
$(call COMPILE_DOCS,EEx,eex,EEx,--config "lib/mix/docs.exs")
$(call COMPILE_DOCS,EEx,eex,EEx)
docs_mix: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (mix)"
$(Q) rm -rf doc/mix
$(call COMPILE_DOCS,Mix,mix,Mix,--config "lib/mix/docs.exs")
$(call COMPILE_DOCS,Mix,mix,Mix)
docs_iex: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (iex)"
$(Q) rm -rf doc/iex
$(call COMPILE_DOCS,IEx,iex,IEx,--config "lib/mix/docs.exs")
$(call COMPILE_DOCS,IEx,iex,IEx)
docs_ex_unit: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (ex_unit)"
$(Q) rm -rf doc/ex_unit
$(call COMPILE_DOCS,ExUnit,ex_unit,ExUnit,--config "lib/mix/docs.exs")
$(call COMPILE_DOCS,ExUnit,ex_unit,ExUnit)
docs_logger: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (logger)"
$(Q) rm -rf doc/logger
$(call COMPILE_DOCS,Logger,logger,Logger,--config "lib/mix/docs.exs")
$(call COMPILE_DOCS,Logger,logger,Logger)
../ex_doc/bin/ex_doc:
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
@ false
#==> Zip tasks
#==> Zips
Docs.zip: docs
rm -f Docs.zip
zip -9 -r Docs.zip CHANGELOG.md doc NOTICE LICENSE README.md
@ echo "Docs file created $(CURDIR)/Docs.zip"
rm -rf Docs-v$(VERSION).zip
zip -9 -r Docs-v$(VERSION).zip CHANGELOG.md doc NOTICE LICENSE README.md
@ echo "Docs file created $(CURDIR)/Docs-v$(VERSION).zip"
Precompiled.zip: build_man compile
rm -f Precompiled.zip
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man NOTICE README.md VERSION
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
rm -rf Precompiled-v$(VERSION).zip
zip -9 -r Precompiled-v$(VERSION).zip bin CHANGELOG.md lib/*/ebin LICENSE man NOTICE README.md VERSION
@ echo "Precompiled file created $(CURDIR)/Precompiled-v$(VERSION).zip"
#==> Test tasks
#==> Publish
# If you modify this task, please update .cirrus.yml accordingly
test: test_formatted test_erlang test_elixir
publish_zips: Precompiled.zip Docs.zip
test_windows: test test_taskkill
publish_docs: docs
rm -rf ../docs/$(DOCS)/*/
cp -R doc/* ../docs/$(DOCS)
test_taskkill:
taskkill //IM erl.exe //F //T //FI "MEMUSAGE gt 0"
taskkill //IM epmd.exe //F //T //FI "MEMUSAGE gt 0"
#==> Tests tasks
test: test_erlang test_elixir
TEST_ERL = lib/elixir/test/erlang
TEST_EBIN = lib/elixir/test/ebin
TEST_ERLS = $(addprefix $(TEST_EBIN)/, $(addsuffix .beam, $(basename $(notdir $(wildcard $(TEST_ERL)/*.erl)))))
define FORMAT
$(Q) if [ "$(OS)" = "Windows_NT" ]; then \
cmd //C call ./bin/mix.bat format $(1); \
else \
bin/elixir bin/mix format $(1); \
fi
endef
format: compile
$(call FORMAT)
test_formatted: compile
$(call FORMAT,--check-formatted)
test_erlang: compile $(TEST_ERLS)
@ echo "==> elixir (eunit)"
$(Q) $(ERL) -pa $(TEST_EBIN) -s test_helper test;
@@ -269,15 +205,15 @@ $(TEST_EBIN)/%.beam: $(TEST_ERL)/%.erl
$(Q) mkdir -p $(TEST_EBIN)
$(Q) $(ERLC) -o $(TEST_EBIN) $<
test_elixir: test_stdlib test_ex_unit test_logger test_eex test_iex test_mix
test_elixir: test_stdlib test_ex_unit test_logger test_mix test_eex test_iex
test_stdlib: compile
@ echo "==> elixir (ex_unit)"
@ echo "==> elixir (exunit)"
$(Q) exec epmd & exit
$(Q) if [ "$(OS)" = "Windows_NT" ]; then \
cd lib/elixir && cmd //C call ../../bin/elixir.bat -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
cd lib/elixir && cmd //C call ../../bin/elixir.bat -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"; \
else \
cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"; \
fi
#==> Dialyzer tasks
@@ -287,7 +223,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 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)
@@ -296,7 +232,7 @@ build_plt: clean_plt $(PLT)
dialyze: compile $(PLT)
@ echo "==> Dialyzing Elixir..."
$(Q) dialyzer -pa lib/elixir/ebin --plt $(PLT) $(DIALYZER_OPTS) lib/*/ebin
$(Q) dialyzer --plt $(PLT) $(DIALYZER_OPTS) lib/*/ebin
#==> Man page tasks
@@ -304,26 +240,24 @@ build_man: man/iex.1 man/elixir.1
man/iex.1:
$(Q) cp man/iex.1.in man/iex.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/r common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) rm -f man/iex.1.bak
$(Q) rm man/iex.1.bak
man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
$(Q) rm man/elixir.1.bak
clean_man:
rm -f man/elixir.1
rm -f man/elixir.1.bak
rm -f man/iex.1
rm -f man/iex.1.bak
install_man: build_man
$(Q) mkdir -p $(DESTDIR)$(MAN_PREFIX)/man1
$(Q) $(INSTALL_DATA) man/elixir.1 $(DESTDIR)$(MAN_PREFIX)/man1
$(Q) $(INSTALL_DATA) man/elixirc.1 $(DESTDIR)$(MAN_PREFIX)/man1
$(Q) $(INSTALL_DATA) man/iex.1 $(DESTDIR)$(MAN_PREFIX)/man1
$(Q) $(INSTALL_DATA) man/mix.1 $(DESTDIR)$(MAN_PREFIX)/man1
"$(MAKE)" clean_man
$(Q) mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/elixir.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/elixirc.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/iex.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/mix.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(MAKE) clean_man
+4 -19
View File
@@ -1,7 +1,9 @@
LEGAL NOTICE INFORMATION
------------------------
All the files in this distribution are copyright to the terms below.
All the files in this distribution are copyright (c) 2012 Plataformatec
covered under Elixir's license (see the file LICENSE) except the cases
below.
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
@@ -11,24 +13,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
== All other files
Copyright 2012 Plataformatec
Copyright 2021 The Elixir Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+65 -171
View File
@@ -1,82 +1,14 @@
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI) [![Build status](https://api.cirrus-ci.com/github/elixir-lang/elixir.svg?branch=main)](https://cirrus-ci.com/github/elixir-lang/elixir)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
![Elixir](https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png)
=========
[![Build Status](https://secure.travis-ci.org/elixir-lang/elixir.svg?branch=master
"Build Status")](https://travis-ci.org/elixir-lang/elixir)
For more about Elixir, installation and documentation,
[check Elixir's website](https://elixir-lang.org/).
[check Elixir's website](http://elixir-lang.org/).
## Policies
## Usage
New releases are announced in the [announcement mailing list][8].
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
and replying to the confirmation email.
All security releases [will be tagged with `[security]`][10]. For more
information, please read our [Security Policy][9].
All interactions in our official communication channels follow our
[Code of Conduct][1].
## Bug reports
For reporting bugs, [visit our issue tracker][2] and follow the steps
for reporting a new issue. **Please disclose security vulnerabilities
privately at elixir-security@googlegroups.com**.
## Issues tracker management
All currently open bugs related to the Elixir repository are listed
in the issues tracker. The Elixir team uses the issues tracker to focus
on *actionable items*, including planned enhancements in the short- and
medium-term. We also do our best to label entries for clarity and to ease
collaboration.
Our *actionable item policy* has some important consequences, such as:
* Proposing new features as well as request for support, help, and
guidance must be done in their own spaces, detailed next.
* Issues where we have identified to be outside of Elixir scope,
such as a bug upstream, will be closed (and requested to be moved
elsewhere if appropriate).
* We actively close unrelated and non-actionable issues to keep the
issues tracker tidy. However, we may get things wrong from time to
time, so we are glad to revisit issues and reopen if necessary.
Keep the tone positive and be kind! For more information, see the
[Code of Conduct][1].
### Proposing new features
For proposing new features, please start a discussion in the
[Elixir Core mailing list][3]. Keep in mind that it is your responsibility
to argue and explain why a feature is useful and how it will impact the
codebase and the community.
Once a proposal is accepted, it will be added to [the issue tracker][2].
Features and bug fixes that have already been merged and will be included
in the next release are then "closed" and added to the [changelog][7].
### Discussions, support, and help
For general discussions, support, and help, please use many of the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available
to help you.
## Compiling from source
For the many different ways to install Elixir,
[see our installation instructions on the website](https://elixir-lang.org/install.html).
However, if you want to contribute to Elixir, you will need to compile from source.
First, [install Erlang](https://elixir-lang.org/install.html#installing-erlang).
After that, clone this repository to your machine, compile and test it:
To run Elixir from source, clone this repository to your machine, compile and test it:
```sh
git clone https://github.com/elixir-lang/elixir.git
@@ -88,37 +20,47 @@ make clean test
[this article includes important notes for compiling Elixir from source
on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
In case you want to use this Elixir version as your system version,
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
If Elixir fails to build (specifically when pulling in a new version via
`git`), be sure to remove any previous build artifacts by running
`make clean`, then `make test`.
If tests pass, you are ready to move on to the
[Getting Started guide][1] or to try Interactive Elixir by running:
`bin/iex` in your terminal.
However, if tests fail, it is likely you have an outdated Erlang version
(Elixir requires Erlang 18.0 or later). You can check your Erlang version
by calling `erl` in the command line. You will see some information as follows:
Erlang/OTP 18 [erts-7.0] [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
If you have the correct version and tests still fail, please
[open an issue][2].
## Contributing
We welcome everyone to contribute to Elixir. To do so, there are a few
things you need to know about the code. First, Elixir code is divided
in applications inside the `lib` folder:
We welcome everyone to contribute to Elixir and help us tackle
existing issues! To do so, there are a few things you need to know
about the code. First, Elixir code is divided in applications inside
the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `elixir` - Contains Elixir's kernel and stdlib
* `eex` - EEx is the template engine that allows you to embed Elixir
* `eex` - Template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `ex_unit` - Simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `iex` - IEx, Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `logger` - The built-in logger
* `mix` - Mix is Elixir's build tool
* `mix` - Elixir's build tool
You can run all tests in the root directory with `make test` and you can
also run tests for a specific framework `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
also run tests for a specific framework `make test_#{NAME}`, for example,
`make test_ex_unit`.
If you are changing just one file, you can choose to compile and run tests only
In case you are changing a single file, you can compile and run tests only
for that particular file for fast development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
@@ -127,66 +69,28 @@ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile (including Erlang modules):
```sh
make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted and then run the full suite with
After your changes are done, please remember to run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
you can rebuild the language from scratch with:
```sh
make clean_elixir compile
```
Similarly, if you can't get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
From time to time, your tests may fail in an existing Elixir checkout and
may require a clean start by running `make clean compile`. You can always
check [the official build status on Travis-CI](https://travis-ci.org/elixir-lang/elixir).
With tests running and passing, you are ready to contribute to Elixir and
[send a pull request](https://help.github.com/articles/using-pull-requests/).
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Implement Enum.member? – Pull Request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? – Pull Request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit – Pull Request](https://github.com/elixir-lang/elixir/pull/1059)
### Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If any changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
We usually keep a list of enhancements and bugs [in the issue tracker][2].
For proposing a new feature, please start a discussion in the
[Elixir Core mailing list][3]. Keep in mind that it is your responsibility
to argue and explain why a feature is useful and how it will impact the
codebase and the community. Finally, remember all interactions in our official
spaces follow our [Code of Conduct][7].
## Building documentation
@@ -195,47 +99,37 @@ to be installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/mix do deps.get + compile
git clone git://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/mix do deps.get, compile
cd ../elixir && make docs
```
Now go back to Elixir's root directory and run:
```sh
make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
This will produce documentation sets for `elixir`, `mix`, etc., under
the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](http://elixir-lang.org/docs/stable/elixir/writing-documentation.html).
## Development links
* [Elixir Website][1]
* [Elixir Documentation][6]
* [Elixir Core Mailing list (development)][3]
* [Announcement mailing list][8]
* [Code of Conduct][1]
* [Issue tracker][2]
* [Changelog][7]
* [Security Policy][9]
* **[#elixir][4]** on [Libera.Chat][5] IRC
* [Issues tracker][2]
* [Code of Conduct][7]
* **[#elixir-lang][4]** on [Freenode][5] IRC
[1]: CODE_OF_CONDUCT.md
[1]: http://elixir-lang.org
[2]: https://github.com/elixir-lang/elixir/issues
[3]: https://groups.google.com/group/elixir-lang-core
[4]: https://web.libera.chat/#elixir
[5]: https://libera.chat
[6]: https://elixir-lang.org/docs.html
[7]: CHANGELOG.md
[8]: https://groups.google.com/group/elixir-lang-ann
[9]: SECURITY.md
[10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date
[4]: https://webchat.freenode.net/?channels=#elixir-lang
[5]: http://www.freenode.net
[6]: http://elixir-lang.org/docs.html
[7]: CODE_OF_CONDUCT.md
## License
"Elixir" and the Elixir logo are registered trademarks of The Elixir Team.
"Elixir" and the Elixir logo are copyright (c) 2012 Plataformatec.
Elixir source code is released under Apache License 2.0.
Elixir source code is released under Apache 2 License.
Check [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more information.
Check [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more
information.
+25 -25
View File
@@ -1,49 +1,49 @@
# Release process
## Shipping a new version
## All releases
This document simply outlines the release process:
1. Ensure you are running on the oldest supported Erlang version
2. Update version in /VERSION, bin/elixir and bin/elixir.bat
2. Remove all `-dev` extension from versions (see below for all files)
3. Ensure /CHANGELOG.md is updated, versioned and add the current date
3. Ensure CHANGELOG is updated and add current date
4. Update "Compatibility and Deprecations" if a new OTP version is supported
4. Commit changes above with title "Release vVERSION" and generate new tag
5. Commit changes above with title "Release vVERSION", generate a new tag, and push it
5. Run `make clean test` to ensure all tests pass from scratch and the CI is green
6. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
6. Recompile an existing project (for example, Ecto) to ensure manifests can be upgraded
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
7. Push branch and the new tag
8. Add the release to `elixir.csv` with the minimum supported OTP version (all releases), update `erlang.csv` to the latest supported OTP version, and `_data/elixir-versions.yml` (except for RCs) files in `elixir-lang/elixir-lang.github.com`
8. Publish new docs with `make publish_docs`, copy docs to `docs/stable` if appropriate, and push to GitHub Pages
## Creating a new vMAJOR.MINOR branch
9. Publish new zips with `make publish_zips`, upload `Precompiled.zip` and `Docs.zip` to GitHub Releases
### In the new branch
10. Add the release to `elixir.csv` file in `elixir-lang/elixir-lang.github.com`
1. Set `CANONICAL=` in /Makefile
## New vMAJOR.MINOR releases
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
11. Create a new branch "vMAJOR.MINOR"
3. Commit "Branch out vMAJOR.MINOR"
12. Move docs generation to `docs/vMAJOR.MINOR` in Makefile, set CANONICAL to stable and copy them to `docs/stable` (change index.html accordingly)
### Back in main
13. In master, bump versions, start new CHANGELOG, add `-dev` back and commit "Start vVERSION+1"
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
## Places where version is mentioned
2. Start new /CHANGELOG.md
* VERSION
* CHANGELOG.md
* src/elixir.app.src (not lib/elixir/src/elixir.app.src)
3. Update tables in /SECURITY.md and in "Compatibility and Deprecations"
## Deprecation policy
4. Commit "Start vMAJOR.MINOR+1"
Elixir deprecations happens in 3 steps:
## Changing supported Erlang/OTP versions
1. The feature is soft-deprecated. It means both CHANGELOG and documentation must list the feature as deprecated but no warning is effectively emitted by running the code. There is no requirement to soft-deprecate a feature.
1. Update the table in Compatibility and Deprecations
2. The feature is effectively deprecated by emitting warnings on usage. In order to deprecate a feature, the proposed alternative MUST exist for AT LEAST two versions. For example, `Enum.uniq/2` was soft-deprecated in favor of `Enum.uniq_by/2` in Elixir v1.1. This means a deprecation warning may only be emitted by Elixir v1.3 or later.
2. Update `otp_release` checks in /Makefile and `/lib/elixir/src/elixir.erl`
3. Update CI workflows in `/.cirrus.yml`, `/.github/workflows/ci.yml`, and `/.github/workflows/releases.yml`
4. Remove `otp_release` version checks that are no longer needed
3. The feature is removed. This can only happen on major releases. This means deprecated features in Elixir v1.x shall only be removed by Elixir v2.x.
-23
View File
@@ -1,23 +0,0 @@
# Security Policy
## Supported versions
Elixir applies bug fixes only to the latest minor branch. Security patches are available for the last 5 minor branches:
Elixir version | Support
:------------- | :-----------------------------
1.14 | Bug fixes and security patches
1.13 | Security patches only
1.12 | Security patches only
1.11 | Security patches only
1.10 | Security patches only
## Announcements
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email.
Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
## Reporting a vulnerability
Please disclose security vulnerabilities privately at elixir-security@googlegroups.com
+1 -1
View File
@@ -1 +1 @@
1.14.5
1.3.2
+69 -192
View File
@@ -1,62 +1,30 @@
#!/bin/sh
set -e
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "Usage: `basename $0` [options] [.exs file] [data]
ELIXIR_VERSION=1.14.5
-v Prints version and exits
-e COMMAND Evaluates the given command (*)
-r FILE Requires the given files/patterns (*)
-S SCRIPT   Finds and executes the given script in PATH
-pr FILE Requires the given files/patterns in parallel (*)
-pa PATH Prepends the given path to Erlang code path (*)
-pz PATH Appends the given path to Erlang code path (*)
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
Usage: $(basename "$0") [options] [.exs file] [data]
--app APP Starts the given app and its dependencies (*)
--cookie COOKIE Sets a cookie for this distributed node
--detached Starts the Erlang VM detached from console
--erl SWITCHES Switches to be passed down to Erlang (*)
--hidden Makes a hidden node
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--name NAME Makes and assigns a name to the distributed node
--no-halt Does not halt the Erlang VM after execution
--sname NAME Makes and assigns a short name to the distributed node
--werl Uses Erlang's Windows shell GUI (Windows only)
## General options
-e "COMMAND" Evaluates the given command (*)
-h, --help Prints this message (standalone)
-r "FILE" Requires the given files/patterns (*)
-S SCRIPT Finds and executes the given script in \$PATH
-pr "FILE" Requires the given files/patterns in parallel (*)
-pa "PATH" Prepends the given path to Erlang code path (*)
-pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
--app APP Starts the given app and its dependencies (*)
--erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--no-halt Does not halt the Erlang VM after execution
--short-version Prints Elixir version (standalone)
--werl Uses Erlang's Windows shell GUI (Windows only)
Options given after the .exs file or -- are passed down to the executed code.
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
## Distribution options
The following options are related to node distribution.
--cookie COOKIE Sets a cookie for this distributed node
--hidden Makes a hidden node
--name NAME Makes and assigns a name to the distributed node
--rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*)
--sname NAME Makes and assigns a short name to the distributed node
## Release options
The following options are generally used under releases.
--boot "FILE" Uses the given FILE.boot to start the system
--boot-var VAR "VALUE" Makes \$VAR available as VALUE to FILE.boot (*)
--erl-config "FILE" Loads configuration in FILE.config written in Erlang (*)
--pipe-to "PIPEDIR" "LOGDIR" Starts the Erlang VM as a named PIPEDIR and LOGDIR
--vm-args "FILE" Passes the contents in file as arguments to the VM
--pipe-to starts Elixir detached from console (Unix-like only).
It will attempt to create PIPEDIR and LOGDIR if they don't exist.
See run_erl to learn more. To reattach, run: to_erl PIPEDIR.
** Options marked with (*) can be given more than once.
** Standalone options can't be combined with other options.
USAGE
** Options marked with (*) can be given more than once
** Options given after the .exs file or -- are passed down to the executed code
** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS or --erl" >&2
exit 1
fi
@@ -66,152 +34,70 @@ readlink_f () {
if [ -h "$filename" ]; then
readlink_f "$(readlink "$filename")"
else
echo "$(pwd -P)/$filename"
echo "`pwd -P`/$filename"
fi
}
if [ $# -eq 1 ] && [ "$1" = "--short-version" ]; then
echo "$ELIXIR_VERSION"
exit 0
fi
# Stores static Erlang arguments and --erl (which is passed as is)
ERL=""
# Stores erl arguments preserving spaces/quotes (mimics an array)
erl_set () {
eval "E${E}=\$1"
E=$((E + 1))
}
# Checks if a string starts with prefix. Usage: starts_with "$STRING" "$PREFIX"
starts_with () {
case $1 in
"$2"*) true;;
*) false;;
esac
}
ERL_EXEC="erl"
MODE="elixir"
ERL_EXEC="erl"
ERL=""
I=1
E=0
LENGTH=$#
set -- "$@" -extra
while [ $I -le $LENGTH ]; do
# S counts to be shifted, C counts to be copied
S=0
C=0
case "$1" in
while [ $I -le $# ]; do
S=1
eval "PEEK=\${$I}"
case "$PEEK" in
+iex)
C=1
MODE="iex"
;;
+elixirc)
C=1
MODE="elixirc"
;;
-v|--no-halt|--no-pry)
C=1
-v|--compile|--no-halt)
;;
-e|-r|-pr|-pa|-pz|--app|--eval|--remsh|--dot-iex)
C=2
-e|-r|-pr|-pa|-pz|--remsh|--app)
S=2
;;
--rpc-eval)
C=3
--detached|--hidden)
ERL="$ERL `echo $PEEK | cut -c 2-`"
;;
--hidden)
S=1
ERL="$ERL -hidden"
--cookie)
I=$(expr $I + 1)
eval "VAL=\${$I}"
ERL="$ERL -setcookie "$VAL""
;;
--sname|--name)
I=$(expr $I + 1)
eval "VAL=\${$I}"
ERL="$ERL `echo $PEEK | cut -c 2-` "$VAL""
;;
--logger-otp-reports)
S=2
if [ "$2" = 'true' ] || [ "$2" = 'false' ]; then
ERL="$ERL -logger handle_otp_reports $2"
I=$(expr $I + 1)
eval "VAL=\${$I}"
if [ "$VAL" == 'true' ] || [ "$VAL" == 'false' ]; then
ERL="$ERL -logger handle_otp_reports "$VAL""
fi
;;
--logger-sasl-reports)
S=2
if [ "$2" = 'true' ] || [ "$2" = 'false' ]; then
ERL="$ERL -logger handle_sasl_reports $2"
I=$(expr $I + 1)
eval "VAL=\${$I}"
if [ "$VAL" == 'true' ] || [ "$VAL" == 'false' ]; then
ERL="$ERL -logger handle_sasl_reports "$VAL""
fi
;;
--erl)
S=2
ERL="$ERL $2"
;;
--cookie)
S=2
erl_set "-setcookie"
erl_set "$2"
;;
--sname|--name)
S=2
erl_set "$(echo "$1" | cut -c 2-)"
erl_set "$2"
;;
--erl-config)
S=2
erl_set "-config"
erl_set "$2"
;;
--vm-args)
S=2
erl_set "-args_file"
erl_set "$2"
;;
--boot)
S=2
erl_set "-boot"
erl_set "$2"
;;
--boot-var)
S=3
erl_set "-boot_var"
erl_set "$2"
erl_set "$3"
;;
--pipe-to)
S=3
RUN_ERL_PIPE="$2"
RUN_ERL_LOG="$3"
if [ "$(starts_with "$RUN_ERL_PIPE" "-")" ]; then
echo "--pipe-to : PIPEDIR cannot be a switch" >&2 && exit 1
elif [ "$(starts_with "$RUN_ERL_LOG" "-")" ]; then
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
fi
I=$(expr $I + 1)
eval "VAL=\${$I}"
ERL="$ERL "$VAL""
;;
--werl)
S=1
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
USE_WERL=true
;;
*)
while [ $I -le $LENGTH ]; do
I=$((I + 1))
set -- "$@" "$1"
shift
done
break
;;
esac
while [ $I -le $LENGTH ] && [ $C -gt 0 ]; do
C=$((C - 1))
I=$((I + 1))
set -- "$@" "$1"
shift
done
I=$((I + S))
shift $S
done
I=$((E - 1))
while [ $I -ge 0 ]; do
eval "VAL=\$E$I"
set -- "$VAL" "$@"
I=$((I - 1))
I=$(expr $I + $S)
done
SELF=$(readlink_f "$0")
@@ -220,30 +106,21 @@ SCRIPT_PATH=$(dirname "$SELF")
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
if [ "$MODE" != "iex" ]; then ERL="-noshell -s elixir start_cli $ERL"; fi
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
# Check for terminal support
if [ "$OS" != "Windows_NT" ]; then
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
fi
# One MAY change ERTS_BIN= but you MUST NOT change
# ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases.
ERTS_BIN=
ERTS_BIN="$ERTS_BIN"
set -- "$ERTS_BIN$ERL_EXEC" -pa "$SCRIPT_PATH"/../lib/*/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
if [ -n "$RUN_ERL_PIPE" ]; then
ESCAPED=""
for PART in "$@"; do
ESCAPED="$ESCAPED $(echo "$PART" | sed 's/[^a-zA-Z0-9_\-\/]/\\&/g')"
done
mkdir -p "$RUN_ERL_PIPE"
mkdir -p "$RUN_ERL_LOG"
ERL_EXEC="run_erl"
set -- "$ERTS_BIN$ERL_EXEC" -daemon "$RUN_ERL_PIPE/" "$RUN_ERL_LOG/" "$ESCAPED"
if [ "$OS" = "Windows_NT" ] && [ $USE_WERL ]; then
ERL_EXEC="werl"
fi
if [ -n "$ELIXIR_CLI_DRY_RUN" ]; then
echo "$@"
else
exec "$@"
if [ -z "$ERL_PATH" ]; then
if [ -f "$SCRIPT_PATH/../releases/RELEASES" ] && [ -f "$SCRIPT_PATH/erl" ]; then
ERL_PATH="$SCRIPT_PATH"/"$ERL_EXEC"
else
ERL_PATH="$ERL_EXEC"
fi
fi
exec "$ERL_PATH" -pa "$SCRIPT_PATH"/../lib/*/ebin $ELIXIR_ERL_OPTIONS $ERL -extra "$@"
+74 -155
View File
@@ -1,193 +1,112 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
set ELIXIR_VERSION=1.14.5
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
if /I ""%1""==""/h"" if ""%2""=="""" goto documentation
if ""%1""==""/?"" if ""%2""=="""" goto documentation
if /I ""%1""==""--short-version"" if ""%2""=="""" goto shortversion
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
if ""%1""=="""" goto :documentation
if /I ""%1""==""--help"" goto :documentation
if /I ""%1""==""-h"" goto :documentation
if /I ""%1""==""/h"" goto :documentation
if ""%1""==""/?"" goto :documentation
goto parseopts
:documentation
echo Usage: %~nx0 [options] [.exs file] [data]
echo.
echo ## General options
echo -v Prints version and exits
echo -e COMMAND Evaluates the given command (*)
echo -r FILE Requires the given files/patterns (*)
echo -S SCRIPT Finds and executes the given script in PATH
echo -pr FILE Requires the given files/patterns in parallel (*)
echo -pa PATH Prepends the given path to Erlang code path (*)
echo -pz PATH Appends the given path to Erlang code path (*)
echo.
echo -e "COMMAND" Evaluates the given command (*)
echo -h, --help Prints this message (standalone)
echo -r "FILE" Requires the given files/patterns (*)
echo -S SCRIPT Finds and executes the given script in $PATH
echo -pr "FILE" Requires the given files/patterns in parallel (*)
echo -pa "PATH" Prepends the given path to Erlang code path (*)
echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo --app APP Starts the given app and its dependencies (*)
echo --cookie COOKIE Sets a cookie for this distributed node
echo --detached Starts the Erlang VM detached from console
echo --erl SWITCHES Switches to be passed down to Erlang (*)
echo --hidden Makes a hidden node
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --name NAME Makes and assigns a name to the distributed node
echo --no-halt Does not halt the Erlang VM after execution
echo --sname NAME Makes and assigns a short name to the distributed node
echo --werl Uses Erlang's Windows shell GUI
echo.
echo --app APP Starts the given app and its dependencies (*)
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --no-halt Does not halt the Erlang VM after execution
echo --short-version Prints Elixir version (standalone)
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Options given after the .exs file or -- are passed down to the executed code.
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
echo.
echo ## Distribution options
echo.
echo The following options are related to node distribution.
echo.
echo --cookie COOKIE Sets a cookie for this distributed node
echo --hidden Makes a hidden node
echo --name NAME Makes and assigns a name to the distributed node
echo --rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*)
echo --sname NAME Makes and assigns a short name to the distributed node
echo.
echo ## Release options
echo.
echo The following options are generally used under releases.
echo.
echo --boot "FILE" Uses the given FILE.boot to start the system
echo --boot-var VAR "VALUE" Makes $VAR available as VALUE to FILE.boot (*)
echo --erl-config "FILE" Loads configuration in FILE.config written in Erlang (*)
echo --vm-args "FILE" Passes the contents in file as arguments to the VM
echo.
echo --pipe-to is not supported on Windows. If set, Elixir won't boot.
echo.
echo ** Options marked with (*) can be given more than once.
echo ** Standalone options can't be combined with other options.
goto end
:shortversion
echo !ELIXIR_VERSION!
echo ** Options marked with (*) can be given more than once
echo ** Options given after the .exs file or -- are passed down to the executed code
echo ** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS or --erl
goto end
:parseopts
rem Parameters for Elixir
set parsElixir=
rem Parameters for Erlang
set parsErlang=
rem Make sure we keep a copy of all parameters
set allPars=%*
rem Get the original path name from the batch file
set originPath=%~dp0
rem Optional parameters before the "-extra" parameter
set beforeExtra=
rem Option which determines whether the loop is over
set endLoop=0
rem Flag which determines whether or not to use werl vs erl
set useWerl=0
rem Designates which mode / Elixir component to run as
set runMode="elixir"
rem Designates the path to the current script
set SCRIPT_PATH=%~dp0
rem Designates the path to the ERTS system
set ERTS_BIN=
set ERTS_BIN=!ERTS_BIN!
rem Recursive loop called for each parameter that parses the cmd line parameters
:startloop
set "par=%~1"
if "!par!"=="" (
rem skip if no parameter
goto expand_erl_libs
)
set par="%1"
shift
set par="!par:"=\"!"
if !endLoop! == 1 (
set parsElixir=!parsElixir! !par!
goto startloop
if "%par%"=="" (
rem if no parameters defined
goto :expand_erl_libs
)
if "%par%"=="""" (
rem if no parameters defined - special case for parameter that is already quoted
goto :expand_erl_libs
)
rem ******* EXECUTION OPTIONS **********************
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set runMode="iex" && goto startloop)
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && set runMode="elixirc" && goto startloop)
rem ******* EVAL PARAMETERS ************************
if ""==!par:-e=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--eval=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--rpc-eval=! (
set "VAR=%~2"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
shift
shift
goto startloop
)
IF "%par%"==""--werl"" (Set useWerl=1)
IF "%par%"==""+iex"" (Set runMode="iex")
rem ******* ELIXIR PARAMETERS **********************
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %1" && shift && goto startloop)
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %1" && shift && goto startloop)
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %1" && shift && goto startloop)
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %1" && shift && goto startloop)
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
if ""==!par:--app=! (set "parsElixir=!parsElixir! --app %1" && shift && goto startloop)
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %1" && shift && goto startloop)
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %1" && shift && goto startloop)
if ""==!par:--no-pry=! (set "parsElixir=!parsElixir! --no-pry" && goto startloop)
rem Note: we don't have to do anything with options that don't take an argument
IF """"=="%par:-e=%" (shift)
IF """"=="%par:-r=%" (shift)
IF """"=="%par:-pr=%" (shift)
IF """"=="%par:-pa=%" (shift)
IF """"=="%par:-pz=%" (shift)
IF """"=="%par:--app=%" (shift)
IF """"=="%par:--remsh=%" (shift)
rem ******* ERLANG PARAMETERS **********************
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot %1" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var %1 %2" && shift && shift && goto startloop)
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie %1" && shift && goto startloop)
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
if ""==!par:--detached=! (set "parsErlang=!parsErlang! -detached" && echo warning: the --detached option is deprecated && goto startloop)
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config %1" && shift && goto startloop)
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name %1" && shift && goto startloop)
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname %1" && shift && goto startloop)
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file %1" && shift && goto startloop)
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
set endLoop=1
set parsElixir=!parsElixir! !par!
goto startloop
IF """"=="%par:--detached=%" (Set parsErlang=%parsErlang% -detached)
IF """"=="%par:--hidden=%" (Set parsErlang=%parsErlang% -hidden)
IF """"=="%par:--cookie=%" (Set parsErlang=%parsErlang% -setcookie %1 && shift)
IF """"=="%par:--sname=%" (Set parsErlang=%parsErlang% -sname %1 && shift)
IF """"=="%par:--name=%" (Set parsErlang=%parsErlang% -name %1 && shift)
IF """"=="%par:--logger-otp-reports=%" (Set parsErlang=%parsErlang% -logger handle_otp_reports %1 && shift)
IF """"=="%par:--logger-sasl-reports=%" (Set parsErlang=%parsErlang% -logger handle_sasl_reports %1 && shift)
IF """"=="%par:--erl=%" (Set beforeExtra=%beforeExtra% %~1 && shift)
goto:startloop
rem ******* assume all pre-params are parsed ********************
:expand_erl_libs
rem expand all ebin paths as Windows does not support the ..\*\ebin wildcard
rem ******* expand all ebin paths as Windows does not support the ..\*\ebin wildcard ********************
SETLOCAL enabledelayedexpansion
set ext_libs=
for /d %%d in ("!SCRIPT_PATH!..\lib\*.") do (
for /d %%d in ("%originPath%..\lib\*.") do (
set ext_libs=!ext_libs! -pa "%%~fd\ebin"
)
SETLOCAL disabledelayedexpansion
: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%
)
if not !runMode! == "iex" (
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
IF %useWerl% EQU 1 (
start werl.exe %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) ELSE (
erl.exe %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
if defined ELIXIR_CLI_DRY_RUN (
if defined useWerl (
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
) else (
if defined useWerl (
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
)
exit /B %ERRORLEVEL%
:end
endlocal
+11 -19
View File
@@ -1,25 +1,17 @@
#!/bin/sh
set -e
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
cat <<USAGE >&2
Usage: $(basename "$0") [elixir switches] [compiler switches] [.ex files]
echo "Usage: `basename $0` [elixir switches] [compiler switches] [.ex files]
-h, --help Prints this message and exits
-o The directory to output compiled files
-v, --version Prints Elixir version and exits (standalone)
-o The directory to output compiled files
--no-docs Do not attach documentation to compiled modules
--no-debug-info Do not attach debug info to compiled modules
--verbose Print compilation status
--warnings-as-errors Treat warnings as errors and return non-zero exit code
--ignore-module-conflict
--ignore-module-conflict Does not emit warnings if a module was previously defined
--no-debug-info Does not attach debug info to compiled modules
--no-docs Does not attach documentation to compiled modules
--profile time Profile the time to compile modules
--verbose Prints compilation status
--warnings-as-errors Treats warnings as errors and return non-zero exit status
Options given after -- are passed down to the executed code.
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS.
Options can be passed to the Erlang compiler using \$ERL_COMPILER_OPTIONS.
USAGE
** Options given after -- are passed down to the executed code
** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS
** Options can be passed to the Erlang compiler using ERL_COMPILER_OPTIONS" >&2
exit 1
fi
@@ -29,7 +21,7 @@ readlink_f () {
if [ -h "$filename" ]; then
readlink_f "$(readlink "$filename")"
else
echo "$(pwd -P)/$filename"
echo "`pwd -P`/$filename"
fi
}
+12 -16
View File
@@ -1,12 +1,12 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
set argc=0
for %%A in (%*) do (
if /I "%%A"=="--help" goto documentation
if /I "%%A"=="-h" goto documentation
if /I "%%A"=="/h" goto documentation
if "%%A"=="/?" goto documentation
set /A argc+=1
if /I "%%A"=="--help" goto documentation
if /I "%%A"=="-h" goto documentation
if /I "%%A"=="/h" goto documentation
if "%%A"=="/?" goto documentation
set /A argc+=1
)
if %argc%==0 goto documentation
goto run
@@ -14,16 +14,12 @@ goto run
:documentation
echo Usage: %~nx0 [elixir switches] [compiler switches] [.ex files]
echo.
echo -h, --help Prints this message and exits
echo -o The directory to output compiled files
echo -v, --version Prints Elixir version and exits (standalone)
echo.
echo --ignore-module-conflict Does not emit warnings if a module was previously defined
echo --no-debug-info Does not attach debug info to compiled modules
echo --no-docs Does not attach documentation to compiled modules
echo --profile time Profile the time to compile modules
echo --verbose Prints compilation status
echo --warnings-as-errors Treats warnings as errors and returns non-zero exit status
echo -o The directory to output compiled files
echo --no-docs Do not attach documentation to compiled modules
echo --no-debug-info Do not attach debug info to compiled modules
echo --verbose Print compilation status
echo --warnings-as-errors Treat warnings as errors and return non-zero exit code
echo --ignore-module-conflict
echo.
echo ** Options given after -- are passed down to the executed code
echo ** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS
+28 -14
View File
@@ -1,20 +1,34 @@
#!/bin/sh
set -e
if [ $# -gt 0 ] && ([ "$1" = "--help" ] || [ "$1" = "-h" ]); then
echo "Usage: `basename $0` [options] [.exs file] [data]
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
cat <<USAGE >&2
Usage: $(basename "$0") [options] [.exs file] [data]
-v Prints version and exits
-e COMMAND Evaluates the given command (*)
-r FILE Requires the given files/patterns (*)
-S SCRIPT   Finds and executes the given script in PATH
-pr FILE Requires the given files/patterns in parallel (*)
-pa PATH Prepends the given path to Erlang code path (*)
-pz PATH Appends the given path to Erlang code path (*)
The following options are exclusive to IEx:
--app APP Starts the given app and its dependencies (*)
--cookie COOKIE Sets a cookie for this distributed node
--detached Starts the Erlang VM detached from console
--erl SWITCHES Switches to be passed down to Erlang (*)
--hidden Makes a hidden node
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--name NAME Makes and assigns a name to the distributed node
--no-halt Does not halt the Erlang VM after execution
--sname NAME Makes and assigns a short name to the distributed node
--werl Uses Erlang's Windows shell GUI (Windows only)
--dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment.
Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
If FILE is empty, then no file will be loaded.
--remsh NAME Connects to a node using a remote shell.
--no-pry Doesn't start pry sessions when dbg/2 is called.
--dot-iex PATH Overrides default .iex.exs file and uses path instead;
path can be empty, then no file will be loaded
--remsh NAME Connects to a node using a remote shell
It accepts all other options listed by "elixir --help".
USAGE
** Options marked with (*) can be given more than once
** Options given after the .exs file or -- are passed down to the executed code
** Options can be passed to the VM using ELIXIR_ERL_OPTIONS or --erl" >&2
exit 1
fi
@@ -24,10 +38,10 @@ readlink_f () {
if [ -h "$filename" ]; then
readlink_f "$(readlink "$filename")"
else
echo "$(pwd -P)/$filename"
echo "`pwd -P`/$filename"
fi
}
SELF=$(readlink_f "$0")
SCRIPT_PATH=$(dirname "$SELF")
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-noshell -user Elixir.IEx.CLI" +iex "$@"
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-user Elixir.IEx.CLI" +iex "$@"
+30 -14
View File
@@ -1,5 +1,5 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
SETLOCAL
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
if /I ""%1""==""/h"" goto documentation
@@ -9,21 +9,37 @@ goto run
:documentation
echo Usage: %~nx0 [options] [.exs file] [data]
echo.
echo The following options are exclusive to IEx:
echo -v Prints version and exits
echo -e COMMAND Evaluates the given command (*)
echo -r FILE Requires the given files/patterns (*)
echo -S SCRIPT Finds and executes the given script in PATH
echo -pr FILE Requires the given files/patterns in parallel (*)
echo -pa PATH Prepends the given path to Erlang code path (*)
echo -pz PATH Appends the given path to Erlang code path (*)
echo.
echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment.
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo --no-pry Doesn't start pry sessions when dbg/2 is called.
echo --app APP Starts the given app and its dependencies (*)
echo --cookie COOKIE Sets a cookie for this distributed node
echo --detached Starts the Erlang VM detached from console
echo --erl SWITCHES Switches to be passed down to Erlang (*)
echo --hidden Makes a hidden node
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --name NAME Makes and assigns a name to the distributed node
echo --no-halt Does not halt the Erlang VM after execution
echo --sname NAME Makes and assigns a short name to the distributed node
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help".
echo --dot-iex PATH Overrides default .iex.exs file and uses path instead;
echo path can be empty, then no file will be loaded
echo --remsh NAME Connects to a node using a remote shell
echo.
echo ** Options marked with (*) can be given more than once
echo ** Options given after the .exs file or -- are passed down to the executed code
echo ** Options can be passed to the Erlang VM using ELIXIR_ERL_OPTIONS or --erl
goto end
:run
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-noshell -user Elixir.IEx.CLI" +iex %__ELIXIR_IEX_FLAGS% %*
@if defined IEX_WITH_WERL (@set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" +iex --erl "-user Elixir.IEx.CLI" --no-halt %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
ENDLOCAL
+2 -2
View File
@@ -1,3 +1,3 @@
#!/usr/bin/env elixir
Mix.start()
Mix.CLI.main()
Mix.start
Mix.CLI.main
+2 -2
View File
@@ -1,2 +1,2 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
+80 -207
View File
@@ -1,81 +1,48 @@
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
defmodule EEx do
@moduledoc ~S"""
EEx stands for Embedded Elixir.
EEx stands for Embedded Elixir. It allows you to embed
Elixir code inside a string in a robust way.
Embedded Elixir allows you to embed Elixir code inside a string
in a robust way.
iex> EEx.eval_string("foo <%= bar %>", bar: "baz")
iex> EEx.eval_string "foo <%= bar %>", [bar: "baz"]
"foo baz"
This module provides three main APIs for you to use:
## API
1. Evaluate a string (`eval_string/3`) or a file (`eval_file/3`)
This module provides 3 main APIs for you to use:
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 at runtime and not precompiled.
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.
The APIs above support several options, documented below. You may
also pass an engine which customizes how the EEx code is compiled.
## Options
All functions in this module, unless otherwise noted, accept EEx-related
options. They are:
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`.
file the template is read from or to "nofile" when compiling from a string.
* `:engine` - the EEx engine to be used for compilation.
* `:trim` - if `true`, trims whitespace left and right of quotation as
long as at least one newline is present. All subsequent newlines and
spaces are removed but one newline is retained. Defaults to `false`.
* `:parser_options` - (since: 1.13.0) allow customizing the parsed code
that is generated. See `Code.string_to_quoted/2` for available options.
Note that the options `:file`, `:line` and `:column` are ignored if
passed in. Defaults to `Code.get_compiler_option(:parser_options)`
(which defaults to `[]` if not set).
## Tags
EEx supports multiple tags, declared below:
<% Elixir expression: executes code but discards output %>
<%= Elixir expression: executes code and prints result %>
<%% EEx quotation: returns the contents inside the tag as is %>
<%!-- Comments: they are discarded from source --%>
EEx supports additional tags, that may be used by some engines,
but they do not have a meaning by default:
<%| ... %>
<%/ ... %>
* `:trim` - trims whitespace left/right of quotation tags
## Engine
@@ -85,12 +52,38 @@ defmodule EEx do
By default, `EEx` uses the `EEx.SmartEngine` that provides some
conveniences on top of the simple `EEx.Engine`.
### `EEx.SmartEngine`
### Tags
The smart engine uses EEx default rules and adds the `@` construct
for reading template assigns:
`EEx.SmartEngine` supports the following tags:
iex> EEx.eval_string("<%= @foo %>", assigns: [foo: 1])
<% Elixir expression - inline with output %>
<%= Elixir expression - replace with result %>
<%% EEx quotation - returns the contents inside %>
<%# Comments - they are discarded from source %>
All expressions that output something to the template
**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/2` clauses, they are treated the same in EEx and
also require `=` in order to have their result printed:
<%= if true do %>
It is obviously true
<% else %>
This will never appear
<% end %>
Notice that different engines may have different rules
for each tag. Other tags may be added in future versions.
### Macros
`EEx.SmartEngine` also adds some macros to your template.
An example is the `@` macro which allows easy data access
in a template:
iex> EEx.eval_string "<%= @foo %>", assigns: [foo: 1]
"1"
In other words, `<%= @foo %>` translates to:
@@ -101,46 +94,31 @@ defmodule EEx do
required by the template is not specified at compilation time.
"""
@type line :: non_neg_integer
@type column :: non_neg_integer
@type marker :: '=' | '/' | '|' | ''
@type metadata :: %{column: column, line: line}
@type token ::
{:comment, charlist, metadata}
| {:text, charlist, metadata}
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
| {:eof, metadata}
@doc """
Generates a function definition from the given string.
Generates a function definition from the string.
The first argument is the kind of the generated function (`:def` or `:defp`).
The `name` argument is the name that the generated function will have.
`template` is the string containing the EEx template. `args` is a list of arguments
that the generated function will accept. They will be available inside the EEx
template.
The supported `options` are described [in the module docs](#module-options).
The kind (`:def` or `:defp`) must be given, the
function name, its arguments and the compilation options.
## Examples
iex> defmodule Sample do
...> require EEx
...> EEx.function_from_string(:def, :sample, "<%= a + b %>", [:a, :b])
...> EEx.function_from_string :def, :sample, "<%= a + b %>", [:a, :b]
...> end
iex> Sample.sample(1, 2)
"3"
"""
defmacro function_from_string(kind, name, template, args \\ [], options \\ []) do
quote bind_quoted: binding() do
info = Keyword.merge([file: __ENV__.file, line: __ENV__.line], options)
args = Enum.map(args, fn arg -> {arg, [line: info[:line]], nil} end)
compiled = EEx.compile_string(template, info)
defmacro function_from_string(kind, name, source, args \\ [], options \\ []) do
quote bind_quoted: binding do
info = Keyword.merge [file: __ENV__.file, line: __ENV__.line], options
args = Enum.map args, fn arg -> {arg, [line: info[:line]], nil} end
compiled = EEx.compile_string(source, info)
case kind do
:def -> def unquote(name)(unquote_splicing(args)), do: unquote(compiled)
:defp -> defp unquote(name)(unquote_splicing(args)), do: unquote(compiled)
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
:defp -> defp(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
end
end
end
@@ -148,17 +126,12 @@ defmodule EEx do
@doc """
Generates a function definition from the file contents.
The first argument is the kind of the generated function (`:def` or `:defp`).
The `name` argument is the name that the generated function will have.
`file` is the path to the EEx template file. `args` is a list of arguments
that the generated function will accept. They will be available inside the EEx
template.
The kind (`:def` or `:defp`) must be given, the
function name, its arguments and the compilation options.
This function is useful in case you have templates but
you want to precompile inside a module for speed.
The supported `options` are described [in the module docs](#module-options).
## Examples
# sample.eex
@@ -167,112 +140,55 @@ defmodule EEx do
# sample.ex
defmodule Sample do
require EEx
EEx.function_from_file(:def, :sample, "sample.eex", [:a, :b])
EEx.function_from_file :def, :sample, "sample.eex", [:a, :b]
end
# iex
Sample.sample(1, 2)
#=> "3"
Sample.sample(1, 2) #=> "3"
"""
defmacro function_from_file(kind, name, file, args \\ [], options \\ []) do
quote bind_quoted: binding() do
info = Keyword.merge([file: IO.chardata_to_string(file), line: 1], options)
args = Enum.map(args, fn arg -> {arg, [line: 1], nil} end)
quote bind_quoted: binding do
info = Keyword.merge options, [file: file, line: 1]
args = Enum.map args, fn arg -> {arg, [line: 1], nil} end
compiled = EEx.compile_file(file, info)
@external_resource file
@file file
case kind do
:def -> def unquote(name)(unquote_splicing(args)), do: unquote(compiled)
:defp -> defp unquote(name)(unquote_splicing(args)), do: unquote(compiled)
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
:defp -> defp(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
end
end
end
@doc """
Gets a string `source` and generates a quoted expression
Gets a string `source` and generate a quoted expression
that can be evaluated by Elixir or compiled to a function.
This is useful if you want to compile a EEx template into code and inject
that code somewhere or evaluate it at runtime.
The generated quoted code will use variables defined in the template that
will be taken from the context where the code is evaluated. If you
have a template such as `<%= a + b %>`, then the returned quoted code
will use the `a` and `b` variables in the context where it's evaluated. See
examples below.
The supported `options` are described [in the module docs](#module-options).
## Examples
iex> quoted = EEx.compile_string("<%= a + b %>")
iex> {result, _bindings} = Code.eval_quoted(quoted, a: 1, b: 2)
iex> result
"3"
"""
@spec compile_string(String.t(), keyword) :: Macro.t()
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
case tokenize(source, options) do
{:ok, tokens} ->
EEx.Compiler.compile(tokens, options)
{:error, message, %{column: column, line: line}} ->
file = options[:file] || "nofile"
raise EEx.SyntaxError, file: file, line: line, column: column, message: message
end
def compile_string(source, options \\ []) do
EEx.Compiler.compile(source, options)
end
@doc """
Gets a `filename` and generates a quoted expression
Gets a `filename` and generate a quoted expression
that can be evaluated by Elixir or compiled to a function.
This is useful if you want to compile a EEx template into code and inject
that code somewhere or evaluate it at runtime.
The generated quoted code will use variables defined in the template that
will be taken from the context where the code is evaluated. If you
have a template such as `<%= a + b %>`, then the returned quoted code
will use the `a` and `b` variables in the context where it's evaluated. See
examples below.
The supported `options` are described [in the module docs](#module-options).
## Examples
# sample.eex
<%= a + b %>
# In code:
quoted = EEx.compile_file("sample.eex")
{result, _bindings} = Code.eval_quoted(quoted, a: 1, b: 2)
result
#=> "3"
"""
@spec compile_file(Path.t(), keyword) :: Macro.t()
def compile_file(filename, options \\ []) when is_list(options) do
filename = IO.chardata_to_string(filename)
options = Keyword.merge([file: filename, line: 1], options)
def compile_file(filename, options \\ []) do
options = Keyword.merge options, [file: filename, line: 1]
compile_string(File.read!(filename), options)
end
@doc """
Gets a string `source` and evaluate the values using the `bindings`.
The supported `options` are described [in the module docs](#module-options).
## Examples
iex> EEx.eval_string("foo <%= bar %>", bar: "baz")
iex> EEx.eval_string "foo <%= bar %>", [bar: "baz"]
"foo baz"
"""
@spec eval_string(String.t(), keyword, keyword) :: String.t()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
def eval_string(source, bindings \\ [], options \\ []) do
compiled = compile_string(source, options)
do_eval(compiled, bindings, options)
end
@@ -280,64 +196,21 @@ defmodule EEx do
@doc """
Gets a `filename` and evaluate the values using the `bindings`.
The supported `options` are described [in the module docs](#module-options).
## Examples
# sample.eex
foo <%= bar %>
# IEx
EEx.eval_file("sample.eex", bar: "baz")
#=> "foo baz"
# iex
EEx.eval_file "sample.eex", [bar: "baz"] #=> "foo baz"
"""
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename)
options = Keyword.put_new(options, :file, filename)
def eval_file(filename, bindings \\ [], options \\ []) do
options = Keyword.put options, :file, filename
compiled = compile_file(filename, options)
do_eval(compiled, bindings, options)
end
@doc """
Tokenize the given contents according to the given options.
## Options
* `:line` - An integer to start as line. Default is 1.
* `:column` - An integer to start as column. Default is 1.
* `:indentation` - An integer that indicates the indentation. Default is 0.
* `:trim` - Tells the tokenizer to either trim the content or not. Default is false.
* `:file` - Can be either a file or a string "nofile".
## Examples
iex> EEx.tokenize('foo', line: 1, column: 1)
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
## Result
It returns `{:ok, [token]}` where a token is one of:
* `{:text, content, %{column: column, line: line}}`
* `{:expr, marker, content, %{column: column, line: line}}`
* `{:start_expr, marker, content, %{column: column, line: line}}`
* `{:middle_expr, marker, content, %{column: column, line: line}}`
* `{:end_expr, marker, content, %{column: column, line: line}}`
* `{:eof, %{column: column, line: line}}`
Or `{:error, message, %{column: column, line: line}}` in case of errors.
Note new tokens may be added in the future.
"""
@doc since: "1.14.0"
@spec tokenize(IO.chardata(), opts :: keyword) ::
{:ok, [token()]} | {:error, String.t(), metadata()}
def tokenize(contents, opts \\ []) do
EEx.Compiler.tokenize(contents, opts)
end
### Helpers
defp do_eval(compiled, bindings, options) do
+52 -411
View File
@@ -3,415 +3,70 @@ defmodule EEx.Compiler do
# When changing this setting, don't forget to update the docs for EEx
@default_engine EEx.SmartEngine
@h_spaces [?\s, ?\t]
@all_spaces [?\s, ?\t, ?\n, ?\r]
@doc """
Tokenize EEx contents.
"""
def tokenize(contents, opts) when is_binary(contents) do
tokenize(String.to_charlist(contents), opts)
end
def tokenize(contents, opts) when is_list(contents) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
trim = opts[:trim] || false
indentation = opts[:indentation] || 0
column = indentation + (opts[:column] || 1)
state = %{trim: trim, indentation: indentation, file: file}
{contents, line, column} =
(trim && trim_init(contents, line, column, state)) || {contents, line, column}
tokenize(contents, line, column, state, [{line, column}], [])
end
defp tokenize('<%%' ++ t, line, column, state, buffer, acc) do
tokenize(t, line, column + 3, state, [?%, ?< | buffer], acc)
end
defp tokenize('<%!--' ++ t, line, column, state, buffer, acc) do
case comment(t, line, column + 5, state, []) do
{:error, line, column, message} ->
{:error, message, %{line: line, column: column}}
{:ok, new_line, new_column, rest, comments} ->
token = {:comment, Enum.reverse(comments), %{line: line, column: column}}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
end
end
# TODO: Deprecate this on Elixir v1.18
defp tokenize('<%#' ++ t, line, column, state, buffer, acc) do
case expr(t, line, column + 3, state, []) do
{:error, line, column, message} ->
{:error, message, %{line: line, column: column}}
{:ok, _, new_line, new_column, rest} ->
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, & &1)
end
end
defp tokenize('<%' ++ t, line, column, state, buffer, acc) do
{marker, t} = retrieve_marker(t)
case expr(t, line, column + 2 + length(marker), state, []) do
{:error, line, column, message} ->
{:error, message, %{line: line, column: column}}
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, warnings, tokens} ->
Enum.each(Enum.reverse(warnings), fn {location, file, msg} ->
:elixir_errors.erl_warn(location, file, msg)
end)
token_key(tokens, expr)
{:error, _, _, _, _} ->
{:expr, expr}
end
marker =
if key in [:middle_expr, :end_expr] and marker != '' do
message =
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
"please remove \"#{marker}\""
:elixir_errors.erl_warn({line, column}, state.file, message)
''
else
marker
end
token = {key, marker, expr, %{line: line, column: column}}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
end
end
defp tokenize('\n' ++ t, line, _column, state, buffer, acc) do
tokenize(t, line + 1, state.indentation + 1, state, [?\n | buffer], acc)
end
defp tokenize([h | t], line, column, state, buffer, acc) do
tokenize(t, line, column + 1, state, [h | buffer], acc)
end
defp tokenize([], line, column, _state, buffer, acc) do
eof = {:eof, %{line: line, column: column}}
{:ok, Enum.reverse([eof | tokenize_text(buffer, acc)])}
end
defp trim_and_tokenize(rest, line, column, state, buffer, acc, fun) do
{rest, line, column, buffer} = trim_if_needed(rest, line, column, state, buffer)
acc = tokenize_text(buffer, acc)
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
end
# Retrieve marker for <%
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
{[marker], t}
end
defp retrieve_marker(t) do
{'', t}
end
# Tokenize a multi-line comment until we find --%>
defp comment([?-, ?-, ?%, ?> | t], line, column, _state, buffer) do
{:ok, line, column + 4, t, buffer}
end
defp comment('\n' ++ t, line, _column, state, buffer) do
comment(t, line + 1, state.indentation + 1, state, '\n' ++ buffer)
end
defp comment([head | t], line, column, state, buffer) do
comment(t, line, column + 1, state, [head | buffer])
end
defp comment([], line, column, _state, _buffer) do
{:error, line, column, "missing token '--%>'"}
end
# Tokenize an expression until we find %>
defp expr([?%, ?> | t], line, column, _state, buffer) do
{:ok, Enum.reverse(buffer), line, column + 2, t}
end
defp expr('\n' ++ t, line, _column, state, buffer) do
expr(t, line + 1, state.indentation + 1, state, [?\n | buffer])
end
defp expr([h | t], line, column, state, buffer) do
expr(t, line, column + 1, state, [h | buffer])
end
defp expr([], line, column, _state, _buffer) do
{:error, line, column, "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, tokens |> Enum.reverse() |> drop_eol()} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
{_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr)}
{_, [{: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
end
end
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
defp drop_eol(rest), do: rest
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 closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
defp closing_bracket?(_), do: false
# Tokenize the buffered text by appending
# it to the given accumulator.
defp tokenize_text([{_line, _column}], acc) do
acc
end
defp tokenize_text(buffer, acc) do
[{line, column} | buffer] = Enum.reverse(buffer)
[{:text, buffer, %{line: line, column: column}} | acc]
end
## Trim
defp trim_if_needed(rest, line, column, state, buffer) do
if state.trim do
buffer = trim_left(buffer, 0)
{rest, line, column} = trim_right(rest, line, column, 0, state)
{rest, line, column, buffer}
else
{rest, line, column, buffer}
end
end
defp trim_init([h | t], line, column, state) when h in @h_spaces,
do: trim_init(t, line, column + 1, state)
defp trim_init([?\r, ?\n | t], line, _column, state),
do: trim_init(t, line + 1, state.indentation + 1, state)
defp trim_init([?\n | t], line, _column, state),
do: trim_init(t, line + 1, state.indentation + 1, state)
defp trim_init([?<, ?% | _] = rest, line, column, _state),
do: {rest, line, column}
defp trim_init(_, _, _, _), do: false
defp trim_left(buffer, count) do
case trim_whitespace(buffer, 0) do
{[?\n, ?\r | rest], _} -> trim_left(rest, count + 1)
{[?\n | rest], _} -> trim_left(rest, count + 1)
_ when count > 0 -> [?\n | buffer]
_ -> buffer
end
end
defp trim_right(rest, line, column, last_column, state) do
case trim_whitespace(rest, column) do
{[?\r, ?\n | rest], column} ->
trim_right(rest, line + 1, state.indentation + 1, column + 1, state)
{[?\n | rest], column} ->
trim_right(rest, line + 1, state.indentation + 1, column, state)
{[], column} ->
{[], line, column}
_ when last_column > 0 ->
{[?\n | rest], line - 1, last_column}
_ ->
{rest, line, column}
end
end
defp trim_whitespace([h | t], column) when h in @h_spaces, do: trim_whitespace(t, column + 1)
defp trim_whitespace(list, column), do: {list, column}
@doc """
This is the compilation entry point. It glues the tokenizer
and the engine together by handling the tokens and invoking
the engine every time a full expression or text is received.
"""
@spec compile([EEx.token()], keyword) :: Macro.t()
def compile(tokens, opts) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
parser_options = opts[:parser_options] || Code.get_compiler_option(:parser_options)
engine = opts[:engine] || @default_engine
state = %{
engine: engine,
file: file,
line: line,
quoted: [],
start_line: nil,
start_column: nil,
parser_options: parser_options
}
init = state.engine.init(opts)
generate_buffer(tokens, init, [], state)
end
# Ignore tokens related to comment.
defp generate_buffer([{:comment, _chars, _meta} | rest], buffer, scope, state) do
generate_buffer(rest, buffer, scope, state)
end
# Generates the buffers by handling each expression from the tokenizer.
# It returns Macro.t/0 or it raises.
defp generate_buffer([{:text, chars, meta} | rest], buffer, scope, state) do
buffer =
if function_exported?(state.engine, :handle_text, 3) do
meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else
# TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end
generate_buffer(rest, buffer, scope, state)
end
defp generate_buffer([{:expr, mark, chars, meta} | rest], buffer, scope, state) do
options =
[file: state.file, line: meta.line, column: column(meta.column, mark)] ++
state.parser_options
expr = Code.string_to_quoted!(chars, options)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
generate_buffer(rest, buffer, scope, state)
end
defp generate_buffer(
[{:start_expr, mark, chars, meta} | rest],
buffer,
scope,
state
) do
if mark == '' do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
def compile(source, opts) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
trim = opts[:trim] || false
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}
generate_buffer(tokens, state.engine.init(opts), [], state)
{:error, line, message} ->
raise EEx.SyntaxError, line: line, file: file, message: message
end
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
# Generates the buffers by handling each expression from the tokenizer
{contents, rest} =
generate_buffer(
rest,
state.engine.handle_begin(buffer),
[contents | scope],
%{
state
| quoted: [],
line: line,
start_line: meta.line,
start_column: column(meta.column, mark)
}
)
defp generate_buffer([{:text, chars} | t], buffer, scope, state) do
buffer = state.engine.handle_text(buffer, IO.chardata_to_string(chars))
generate_buffer(t, buffer, scope, state)
end
defp generate_buffer([{:expr, line, mark, chars} | t], 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(t, buffer, scope, state)
end
defp generate_buffer([{:start_expr, start_line, mark, chars} | t], buffer, scope, state) do
{contents, line, t} = look_ahead_text(t, start_line, chars)
{contents, t} = generate_buffer(t, "", [contents | scope],
%{state | quoted: [], line: line, start_line: start_line})
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
generate_buffer(t, buffer, scope, state)
end
defp generate_buffer(
[{:middle_expr, '', chars, meta} | rest],
buffer,
[current | scope],
state
) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
state = %{state | line: meta.line}
generate_buffer(rest, state.engine.handle_begin(buffer), [wrapped | scope], state)
defp generate_buffer([{:middle_expr, line, _, chars} | t], buffer, [current | scope], state) do
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
generate_buffer(t, "", [wrapped | scope], %{state | line: line})
end
defp generate_buffer([{:middle_expr, _, chars, meta} | _], _buffer, [], state) do
raise EEx.SyntaxError,
message: "unexpected middle of expression <%#{chars}%>",
file: state.file,
line: meta.line,
column: meta.column
end
defp generate_buffer(
[{:end_expr, '', chars, meta} | rest],
buffer,
[current | _],
state
) do
{wrapped, state} = wrap_expr(current, meta.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)
defp generate_buffer([{:end_expr, line, _, chars} | t], buffer, [current | _], state) do
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
tuples = Code.string_to_quoted!(wrapped, [line: state.start_line, file: state.file])
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
{buffer, t}
end
defp generate_buffer([{:end_expr, _, chars, meta} | _], _buffer, [], state) do
raise EEx.SyntaxError,
message: "unexpected end of expression <%#{chars}%>",
file: state.file,
line: meta.line,
column: meta.column
defp generate_buffer([{:end_expr, line, _, chars} | _], _buffer, [], state) do
raise EEx.SyntaxError, message: "unexpected token #{inspect chars}", file: state.file, line: line
end
defp generate_buffer([{:eof, _meta}], buffer, [], state) do
defp generate_buffer([], buffer, [], state) do
state.engine.handle_body(buffer)
end
defp generate_buffer([{:eof, meta}], _buffer, _scope, state) do
raise EEx.SyntaxError,
message: "unexpected end of string, expected a closing '<% end %>'",
file: state.file,
line: meta.line,
column: meta.column
defp generate_buffer([], _buffer, _scope, state) do
raise EEx.SyntaxError, message: "unexpected end of string, expected a closing '<% end %>'",
file: state.file, line: state.line
end
# Creates a placeholder and wrap it inside the expression block
@@ -420,41 +75,32 @@ defmodule EEx.Compiler do
new_lines = List.duplicate(?\n, line - state.line)
key = length(state.quoted)
placeholder = '__EEX__(' ++ Integer.to_charlist(key) ++ ');'
count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
{count, new_state}
{current ++ placeholder ++ new_lines ++ chars,
%{state | quoted: [{key, buffer} | state.quoted]}}
end
# Look middle expressions that immediately follow a start_expr
# Look text ahead on expressions
defp look_ahead_middle([{:comment, _comment, _meta} | rest], start, contents),
do: look_ahead_middle(rest, start, contents)
defp look_ahead_middle([{:text, text, _meta} | rest], start, contents) do
defp look_ahead_text([{:text, text}, {:middle_expr, line, _, chars} | t]=list, start, contents) do
if only_spaces?(text) do
look_ahead_middle(rest, start, contents ++ text)
{contents ++ text ++ chars, line, t}
else
nil
{contents, start, list}
end
end
defp look_ahead_middle([{:middle_expr, _, chars, meta} | rest], _start, contents) do
{rest, meta.line, contents ++ chars}
end
defp look_ahead_middle(_tokens, _start, _contents) do
nil
defp look_ahead_text(t, start, contents) do
{contents, start, t}
end
defp only_spaces?(chars) do
Enum.all?(chars, &(&1 in @all_spaces))
Enum.all?(chars, &(&1 in [?\s, ?\t, ?\r, ?\n]))
end
# Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
{^key, value} = List.keyfind(quoted, key, 0)
{^key, value} = List.keyfind quoted, key, 0
value
end
@@ -467,15 +113,10 @@ defmodule EEx.Compiler do
end
defp insert_quoted(list, quoted) when is_list(list) do
Enum.map(list, &insert_quoted(&1, quoted))
Enum.map list, &insert_quoted(&1, quoted)
end
defp insert_quoted(other, _quoted) do
other
end
defp column(column, mark) do
# length('<%') == 2
column + 2 + length(mark)
end
end
+74 -153
View File
@@ -2,73 +2,37 @@ defmodule EEx.Engine do
@moduledoc ~S"""
Basic EEx engine that ships with Elixir.
An engine needs to implement all callbacks below.
An engine needs to implement three functions:
This module also ships with a default engine implementation
you can delegate to. See `EEx.SmartEngine` as an example.
* `init(opts)` - returns the initial buffer
* `handle_body(quoted)` - receives the final built quoted
expression, should do final post-processing and return a
quoted expression.
* `handle_text(buffer, text)` - it receives the buffer,
the text and must return a new quoted expression.
* `handle_expr(buffer, marker, expr)` - it receives the buffer,
the marker, the expr and must return a new quoted expression.
The marker is what follows exactly after `<%`. For example,
`<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
as marker. The allowed markers so far are: `""` and `"="`.
Read `handle_expr/3` below for more information about the markers
implemented by default by this engine.
`EEx.Engine` can be used directly if one desires to use the
default implementations for the functions above.
"""
@type state :: term
@doc """
Called at the beginning of every template.
It must return the initial state.
"""
@callback init(opts :: keyword) :: state
@doc """
Called at the end of every template.
It must return Elixir's quoted expressions for the template.
"""
@callback handle_body(state) :: Macro.t()
@doc """
Called for the text/static parts of a template.
It must return the updated state.
"""
@callback handle_text(state, [line: pos_integer, column: pos_integer], text :: String.t()) ::
state
@doc """
Called for the dynamic/code parts of a template.
The marker is what follows exactly after `<%`. For example,
`<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
as marker. The allowed markers so far are:
* `""`
* `"="`
* `"/"`
* `"|"`
Markers `"/"` and `"|"` are only for use in custom EEx engines
and are not implemented by default. Using them without an
appropriate implementation raises `EEx.SyntaxError`.
It must return the updated state.
"""
@callback handle_expr(state, marker :: String.t(), expr :: Macro.t()) :: state
@doc """
Invoked at the beginning of every nesting.
It must return a new state that is used only inside the nesting.
Once the nesting terminates, the current `state` is resumed.
"""
@callback handle_begin(state) :: state
@doc """
Invokes at the end of a nesting.
It must return Elixir's quoted expressions for the nesting.
"""
@callback handle_end(state) :: Macro.t()
@callback init(Keyword.t) :: Macro.t
@callback handle_body(Macro.t) :: Macro.t
@callback handle_text(Macro.t, String.t) :: Macro.t
@callback handle_expr(Macro.t, String.t, Macro.t) :: Macro.t
@doc false
@deprecated "Use explicit delegation to EEx.Engine instead"
defmacro __using__(_) do
quote do
@behaviour EEx.Engine
@@ -77,27 +41,19 @@ defmodule EEx.Engine do
EEx.Engine.init(opts)
end
def handle_body(state) do
EEx.Engine.handle_body(state)
def handle_body(body) do
EEx.Engine.handle_body(body)
end
def handle_begin(state) do
EEx.Engine.handle_begin(state)
def handle_text(buffer, text) do
EEx.Engine.handle_text(buffer, text)
end
def handle_end(state) do
EEx.Engine.handle_end(state)
def handle_expr(buffer, marker, expr) do
EEx.Engine.handle_expr(buffer, marker, expr)
end
def handle_text(state, text) do
EEx.Engine.handle_text(state, [], text)
end
def handle_expr(state, marker, expr) do
EEx.Engine.handle_expr(state, marker, expr)
end
defoverridable EEx.Engine
defoverridable [handle_body: 1, handle_expr: 3, handle_text: 2, init: 1]
end
end
@@ -110,112 +66,77 @@ defmodule EEx.Engine do
This can be added to any custom engine by invoking
`handle_assign/1` with `Macro.prewalk/2`:
def handle_expr(state, token, expr) do
def handle_expr(buffer, token, expr) do
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
super(state, token, expr)
EEx.Engine.handle_expr(buffer, token, expr)
end
"""
@spec handle_assign(Macro.t()) :: Macro.t()
def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
line = meta[:line] || 0
quote(line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name)))
quote line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name))
end
def handle_assign(arg) do
arg
end
@doc false
# TODO: Raise on v2.0
@spec fetch_assign!(Access.t(), Access.key()) :: term | nil
# TODO: Raise on 2.0
def fetch_assign!(assigns, key) do
case Access.fetch(assigns, key) do
{:ok, val} ->
val
:error ->
keys = Enum.map(assigns, &elem(&1, 0))
IO.warn(
"assign @#{key} not available in EEx template. " <>
"Please ensure all assigns are given as options. " <>
"Available assigns: #{inspect(keys)}"
)
IO.warn "assign @#{key} not available in EEx template. " <>
"Please ensure all assigns are given as options. " <>
"Available assigns: #{inspect keys}"
nil
end
end
@doc "Default implementation for `c:init/1`."
@doc """
Returns an empty string as initial buffer.
"""
def init(_opts) do
%{
binary: [],
dynamic: [],
vars_count: 0
}
""
end
@doc "Default implementation for `c:handle_begin/1`."
def handle_begin(state) do
check_state!(state)
%{state | binary: [], dynamic: []}
@doc """
The default implementation simply returns the given expression.
"""
def handle_body(quoted) do
quoted
end
@doc "Default implementation for `c:handle_end/1`."
def handle_end(quoted) do
handle_body(quoted)
@doc """
The default implementation simply concatenates text to the buffer.
"""
def handle_text(buffer, text) do
quote do: unquote(buffer) <> unquote(text)
end
@doc "Default implementation for `c:handle_body/1`."
def handle_body(state) do
check_state!(state)
%{binary: binary, dynamic: dynamic} = state
binary = {:<<>>, [], Enum.reverse(binary)}
dynamic = [binary | dynamic]
{:__block__, [], Enum.reverse(dynamic)}
@doc """
Implements expressions according to the markers.
<% Elixir expression - inline with output %>
<%= Elixir expression - replace with result %>
All other markers are not implemented by this engine.
"""
@spec handle_expr(Macro.t, String.t, Macro.t) :: Macro.t
def handle_expr(buffer, "=", expr) do
quote do
tmp1 = unquote(buffer)
tmp1 <> String.Chars.to_string(unquote(expr))
end
end
@doc "Default implementation for `c:handle_text/3`."
def handle_text(state, _meta, text) do
check_state!(state)
%{binary: binary} = state
%{state | binary: [text | binary]}
end
@doc "Default implementation for `c:handle_expr/3`."
def handle_expr(state, "=", ast) do
check_state!(state)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(:"arg#{vars_count}", __MODULE__)
ast =
quote do
unquote(var) = String.Chars.to_string(unquote(ast))
end
segment =
quote do
unquote(var) :: binary
end
%{state | dynamic: [ast | dynamic], binary: [segment | binary], vars_count: vars_count + 1}
end
def handle_expr(state, "", ast) do
%{dynamic: dynamic} = state
%{state | dynamic: [ast | dynamic]}
end
def handle_expr(_state, marker, _ast) when marker in ["/", "|"] do
raise EEx.SyntaxError,
"unsupported EEx syntax <%#{marker} %> (the syntax is valid but not supported by the current EEx engine)"
end
defp check_state!(%{binary: _, dynamic: _, vars_count: _}), do: :ok
defp check_state!(state) do
raise "unexpected EEx.Engine state: #{inspect(state)}. " <>
"This typically means a bug or an outdated EEx.Engine or tool"
def handle_expr(buffer, "", expr) do
quote do
tmp2 = unquote(buffer)
unquote(expr)
tmp2
end
end
end
+5 -22
View File
@@ -24,35 +24,18 @@ defmodule EEx.SmartEngine do
# sample.ex
defmodule Sample do
require EEx
EEx.function_from_file(:def, :sample, "sample.eex", [:assigns])
EEx.function_from_file :def, :sample, "sample.eex", [:assigns]
end
# iex
Sample.sample(a: 1, b: 2)
#=> "3"
Sample.sample(a: 1, b: 2) #=> "3"
"""
@behaviour EEx.Engine
use EEx.Engine
@impl true
defdelegate init(opts), to: EEx.Engine
@impl true
defdelegate handle_body(state), to: EEx.Engine
@impl true
defdelegate handle_begin(state), to: EEx.Engine
@impl true
defdelegate handle_end(state), to: EEx.Engine
@impl true
defdelegate handle_text(state, meta, text), to: EEx.Engine
@impl true
def handle_expr(state, marker, expr) do
def handle_expr(buffer, mark, expr) do
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
EEx.Engine.handle_expr(state, marker, expr)
super(buffer, mark, expr)
end
end
+215
View File
@@ -0,0 +1,215 @@
defmodule EEx.Tokenizer do
@moduledoc false
@doc """
Tokenizes the given charlist or binary.
It returns {:ok, list} with the following tokens:
* `{:text, contents}`
* `{:expr, line, marker, contents}`
* `{:start_expr, line, marker, contents}`
* `{:middle_expr, line, marker, contents}`
* `{:end_expr, line, marker, contents}`
Or `{:error, line, error}` in case of errors.
"""
def tokenize(bin, line, opts \\ [])
def tokenize(bin, line, opts) when is_binary(bin) do
tokenize(String.to_charlist(bin), line, opts)
end
def tokenize(list, line, opts) do
tokenize(list, line, opts, [], [])
end
defp tokenize('<%%' ++ t, line, opts, buffer, acc) do
tokenize t, line, opts, [?%, ?< | buffer], acc
end
defp tokenize('<%#' ++ t, line, opts, buffer, acc) do
case expr(t, line, []) do
{:error, _, _} = error -> error
{: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, opts, buffer, acc) do
{marker, t} = retrieve_marker(t)
case expr(t, line, []) do
{:error, _, _} = error -> error
{:ok, expr, new_line, rest} ->
token = token_name(expr)
{rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
acc = tokenize_text(buffer, acc)
final = {token, line, marker, Enum.reverse(expr)}
tokenize rest, new_line, opts, [], [final | acc]
end
end
defp tokenize('\n' ++ t, line, opts, buffer, acc) do
tokenize t, line + 1, opts, [?\n | buffer], acc
end
defp tokenize([h | t], line, opts, buffer, acc) do
tokenize t, line, opts, [h | buffer], acc
end
defp tokenize([], _line, _opts, buffer, acc) do
{:ok, Enum.reverse(tokenize_text(buffer, acc))}
end
# Retrieve marker for <%
defp retrieve_marker('=' ++ t) do
{'=', t}
end
defp retrieve_marker(t) do
{'', t}
end
# Tokenize an expression until we find %>
defp expr([?%, ?> | t], line, buffer) do
{:ok, buffer, line, t}
end
defp expr('\n' ++ t, line, buffer) do
expr t, line + 1, [?\n | buffer]
end
defp expr([h | t], line, buffer) do
expr t, line, [h | buffer]
end
defp expr([], line, _buffer) do
{:error, line, "missing token '%>'"}
end
# 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 ")"
defp token_name([h | t]) when h in [?\s, ?\t, ?)] do
token_name(t)
end
defp token_name('od' ++ [h | _]) when h in [?\s, ?\t, ?)] do
:start_expr
end
defp token_name('>-' ++ rest) do
rest = Enum.reverse(rest)
# Tokenize the remaining passing check_terminators as
# false, which relax the tokenizer to not error on
# unmatched pairs. Then, we 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.
case :elixir_tokenizer.tokenize(rest, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, 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('dne' ++ t), do: check_spaces(t, :end_expr)
defp token_name(_) do
:expr
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 [?\s, ?\t])) do
token
else
:expr
end
end
# Tokenize the buffered text by appending
# it to the given accumulator.
defp tokenize_text([], acc) do
acc
end
defp tokenize_text(buffer, acc) do
[{:text, Enum.reverse(buffer)} | acc]
end
# 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
original = {rest, line, buffer}
if opts[:trim] do
case {trim_left(buffer, acc), trim_right(rest, line)} do
{{true, new_buffer}, {true, new_rest, new_line}} ->
{new_rest, new_line, new_buffer}
_ ->
original
end
else
original
end
end
defp trim_left(buffer, acc) do
case {trim_whitespace(buffer), acc} do
{[?\n | _] = trimmed_buffer, _} -> {true, trimmed_buffer}
{[], []} -> {true, []}
_ -> {false, buffer}
end
end
defp trim_right(rest, line) do
case trim_whitespace(rest) do
[?\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 == ?\s or h == ?\t do
trim_whitespace(t)
end
defp trim_whitespace(list) do
list
end
end
+4 -6
View File
@@ -1,11 +1,9 @@
defmodule EEx.MixProject do
defmodule EEx.Mixfile do
use Mix.Project
def project do
[
app: :eex,
version: System.version(),
build_per_environment: false
]
[app: :eex,
version: System.version,
build_per_environment: false]
end
end
+17 -31
View File
@@ -1,59 +1,45 @@
Code.require_file("../test_helper.exs", __DIR__)
Code.require_file "../test_helper.exs", __DIR__
defmodule EEx.SmartEngineTest do
use ExUnit.Case, async: true
import ExUnit.CaptureIO
test "evaluates simple string" do
assert_eval("foo bar", "foo bar")
assert_eval "foo bar", "foo bar"
end
test "evaluates with assigns as keywords" do
assert_eval("1", "<%= @foo %>", assigns: [foo: 1])
assert_eval "1", "<%= @foo %>", assigns: [foo: 1]
end
test "evaluates with assigns as a map" do
assert_eval("1", "<%= @foo %>", assigns: %{foo: 1})
assert_eval "1", "<%= @foo %>", assigns: %{foo: 1}
end
test "error with missing assigns" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<%= @foo %>", assigns: %{})
end)
stderr = capture_io(:stderr, fn ->
assert_eval "", "<%= @foo %>", assigns: %{}
end)
assert stderr =~ "assign @foo not available in EEx template"
end
test "evaluates with loops" do
assert_eval("1\n2\n3\n", "<%= for x <- [1, 2, 3] do %><%= x %>\n<% end %>")
assert_eval "1\n2\n3\n", "<%= for x <- [1, 2, 3] do %><%= x %>\n<% end %>"
end
test "preserves line numbers in assignments" do
result = EEx.compile_string("foo\n<%= @hello %>", engine: EEx.SmartEngine)
test "compiled preserved line numbers" do
result = EEx.compile_string("<%= @hello %>", engine: EEx.SmartEngine)
Macro.prewalk(result, fn
{_left, meta, [_, :hello]} ->
assert Keyword.get(meta, :line) == 2
send(self(), :found)
node ->
node
{_left, meta, _right} ->
assert Keyword.get(meta, :line, 0) in [0, 1]
_ ->
:ok
end)
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
result = EEx.eval_string(actual, binding, file: __ENV__.file)
assert result == expected
end
end
+94 -326
View File
@@ -1,371 +1,154 @@
Code.require_file("../test_helper.exs", __DIR__)
Code.require_file "../test_helper.exs", __DIR__
defmodule EEx.TokenizerTest do
use ExUnit.Case, async: true
@opts [indentation: 0, trim: false]
require EEx.Tokenizer, as: T
test "simple chars lists" do
assert EEx.tokenize('foo', @opts) ==
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
assert T.tokenize('foo', 1) == {:ok, [{:text, 'foo'}]}
end
test "simple strings" do
assert EEx.tokenize("foo", @opts) ==
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
assert T.tokenize("foo", 1) == {:ok, [{:text, 'foo'}]}
end
test "strings with embedded code" do
assert EEx.tokenize('foo <% bar %>', @opts) ==
{:ok,
[
{:text, 'foo ', %{column: 1, line: 1}},
{:expr, '', ' bar ', %{column: 5, line: 1}},
{:eof, %{column: 14, line: 1}}
]}
assert T.tokenize('foo <% bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '', ' bar '}]}
end
test "strings with embedded equals code" do
assert EEx.tokenize('foo <%= bar %>', @opts) ==
{:ok,
[
{:text, 'foo ', %{column: 1, line: 1}},
{:expr, '=', ' bar ', %{column: 5, line: 1}},
{:eof, %{column: 15, line: 1}}
]}
end
test "strings with embedded slash code" do
assert EEx.tokenize('foo <%/ bar %>', @opts) ==
{:ok,
[
{:text, 'foo ', %{column: 1, line: 1}},
{:expr, '/', ' bar ', %{column: 5, line: 1}},
{:eof, %{column: 15, line: 1}}
]}
end
test "strings with embedded pipe code" do
assert EEx.tokenize('foo <%| bar %>', @opts) ==
{:ok,
[
{:text, 'foo ', %{column: 1, line: 1}},
{:expr, '|', ' bar ', %{column: 5, line: 1}},
{:eof, %{column: 15, line: 1}}
]}
assert T.tokenize('foo <%= bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '=', ' bar '}]}
end
test "strings with more than one line" do
assert EEx.tokenize('foo\n<%= bar %>', @opts) ==
{:ok,
[
{:text, 'foo\n', %{column: 1, line: 1}},
{:expr, '=', ' bar ', %{column: 1, line: 2}},
{:eof, %{column: 11, line: 2}}
]}
assert T.tokenize('foo\n<%= bar %>', 1) ==
{:ok, [{:text, 'foo\n'}, {:expr, 2, '=', ' bar '}]}
end
test "strings with more than one line and expression with more than one line" do
string = '''
foo <%= bar
foo <%= bar
baz %>
<% foo %>
'''
baz %>
<% foo %>
'''
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:expr, '=', ' bar\n\nbaz ', %{column: 5, line: 1}},
{:text, '\n', %{column: 7, line: 3}},
{:expr, '', ' foo ', %{column: 1, line: 4}},
{:text, '\n', %{column: 10, line: 4}},
{:eof, %{column: 1, line: 5}}
]
assert EEx.tokenize(string, @opts) == {:ok, exprs}
assert T.tokenize(string, 1) == {:ok, [
{:text, 'foo '},
{:expr, 1, '=', ' bar\n\nbaz '},
{:text, '\n'},
{:expr, 4, '', ' foo '},
{:text, '\n'}
]}
end
test "quotation" do
assert EEx.tokenize('foo <%% true %>', @opts) ==
{:ok,
[
{:text, 'foo <% true %>', %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]}
assert T.tokenize('foo <%% true %>', 1) == {:ok, [
{:text, 'foo <% true %>'}
]}
end
test "quotation with do-end" do
assert EEx.tokenize('foo <%% true do %>bar<%% end %>', @opts) ==
{:ok,
[
{:text, 'foo <% true do %>bar<% end %>', %{column: 1, line: 1}},
{:eof, %{column: 32, line: 1}}
]}
test "quotation with do/end" do
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 ', %{column: 1, line: 1}},
{:expr, '=', ' c ', %{column: 9, line: 1}},
{:text, ' ', %{column: 17, line: 1}},
{:expr, '=', ' d ', %{column: 18, line: 1}},
{:text, ' e %> f', %{column: 26, line: 1}},
{:eof, %{column: 33, line: 1}}
]
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1) == {:ok, [
{:text, 'a <% b '},
{:expr, 1, '=', ' c '},
{:text, ' '},
{:expr, 1, '=', ' d '},
{:text, ' e %> f'}
]}
assert EEx.tokenize('a <%% b <%= c %> <%= d %> e %> f', @opts) == {:ok, exprs}
assert T.tokenize('<%%% a <%%= b %> c %>', 1) == {:ok, [
{:text, '<%% a <%= b %> c %>'}
]}
end
test "improperly formatted quotation with interpolation" do
exprs = [
{:text, '<%% a <%= b %> c %>', %{column: 1, line: 1}},
{:eof, %{column: 22, line: 1}}
]
assert EEx.tokenize('<%%% a <%%= b %> c %>', @opts) == {:ok, exprs}
test "comments" do
assert T.tokenize('foo <%# true %>', 1) == {:ok, [
{:text, 'foo '}
]}
end
test "EEx comments" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
assert EEx.tokenize('foo <%# true %>', @opts) == {:ok, exprs}
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
assert EEx.tokenize('foo <%#\ntrue %>', @opts) == {:ok, exprs}
end
test "EEx comments with do-end" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:text, 'bar', %{column: 19, line: 1}},
{:eof, %{column: 32, line: 1}}
]
assert EEx.tokenize('foo <%# true do %>bar<%# end %>', @opts) == {:ok, exprs}
end
test "EEx comments inside do-end" do
exprs = [
{:start_expr, '', ' if true do ', %{column: 1, line: 1}},
{:text, 'bar', %{column: 31, line: 1}},
{:end_expr, [], ' end ', %{column: 34, line: 1}},
{:eof, %{column: 43, line: 1}}
]
assert EEx.tokenize('<% if true do %><%# comment %>bar<% end %>', @opts) == {:ok, exprs}
exprs = [
{:start_expr, [], ' case true do ', %{column: 1, line: 1}},
{:middle_expr, '', ' true -> ', %{column: 33, line: 1}},
{:text, 'bar', %{column: 46, line: 1}},
{:end_expr, [], ' end ', %{column: 49, line: 1}},
{:eof, %{column: 58, line: 1}}
]
assert EEx.tokenize('<% case true do %><%# comment %><% true -> %>bar<% end %>', @opts) ==
{:ok, exprs}
end
test "EEx multi-line comments" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:comment, ' true ', %{column: 5, line: 1}},
{:text, ' bar', %{column: 20, line: 1}},
{:eof, %{column: 24, line: 1}}
]
assert EEx.tokenize('foo <%!-- true --%> bar', @opts) == {:ok, exprs}
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:comment, ' \ntrue\n ', %{column: 5, line: 1}},
{:text, ' bar', %{column: 6, line: 3}},
{:eof, %{column: 10, line: 3}}
]
assert EEx.tokenize('foo <%!-- \ntrue\n --%> bar', @opts) == {:ok, exprs}
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:comment, ' <%= true %> ', %{column: 5, line: 1}},
{:text, ' bar', %{column: 27, line: 1}},
{:eof, %{column: 31, line: 1}}
]
assert EEx.tokenize('foo <%!-- <%= true %> --%> bar', @opts) == {:ok, exprs}
end
test "Elixir comments" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:expr, [], ' true # this is a boolean ', %{column: 5, line: 1}},
{:eof, %{column: 35, line: 1}}
]
assert EEx.tokenize('foo <% true # this is a boolean %>', @opts) == {:ok, exprs}
end
test "Elixir comments with do-end" do
exprs = [
{:start_expr, [], ' if true do # startif ', %{column: 1, line: 1}},
{:text, 'text', %{column: 27, line: 1}},
{:end_expr, [], ' end # closeif ', %{column: 31, line: 1}},
{:eof, %{column: 50, line: 1}}
]
assert EEx.tokenize('<% if true do # startif %>text<% end # closeif %>', @opts) ==
{:ok, exprs}
test "comments with do/end" do
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == {:ok, [
{:text, 'foo bar'}
]}
end
test "strings with embedded do end" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:start_expr, '', ' if true do ', %{column: 5, line: 1}},
{:text, 'bar', %{column: 21, line: 1}},
{:end_expr, '', ' end ', %{column: 24, line: 1}},
{:eof, %{column: 33, line: 1}}
]
assert EEx.tokenize('foo <% if true do %>bar<% end %>', @opts) == {:ok, exprs}
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == {:ok, [
{:text, 'foo '},
{:start_expr, 1, '', ' if true do '},
{:text, 'bar'},
{:end_expr, 1, '', ' end '}
]}
end
test "strings with embedded -> end" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:start_expr, '', ' cond do ', %{column: 5, line: 1}},
{:middle_expr, '', ' false -> ', %{column: 18, line: 1}},
{:text, 'bar', %{column: 32, line: 1}},
{:middle_expr, '', ' true -> ', %{column: 35, line: 1}},
{:text, 'baz', %{column: 48, line: 1}},
{:end_expr, '', ' end ', %{column: 51, line: 1}},
{:eof, %{column: 60, line: 1}}
]
assert EEx.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', @opts) ==
{:ok, exprs}
end
test "strings with fn-end with newline" do
exprs = [
{:start_expr, '=', ' a fn ->\n', %{column: 1, line: 1}},
{:text, 'foo', %{column: 3, line: 2}},
{:end_expr, [], ' end ', %{column: 6, line: 2}},
{:eof, %{column: 15, line: 2}}
]
assert EEx.tokenize('<%= a fn ->\n%>foo<% end %>', @opts) ==
{:ok, exprs}
end
test "strings with multiple fn-end" do
exprs = [
{:start_expr, '=', ' a fn -> ', %{column: 1, line: 1}},
{:text, 'foo', %{column: 15, line: 1}},
{:middle_expr, '', ' end, fn -> ', %{column: 18, line: 1}},
{:text, 'bar', %{column: 34, line: 1}},
{:end_expr, '', ' end ', %{column: 37, line: 1}},
{:eof, %{column: 46, line: 1}}
]
assert EEx.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', @opts) ==
{:ok, exprs}
end
test "strings with fn-end followed by do block" do
exprs = [
{:start_expr, '=', ' a fn -> ', %{column: 1, line: 1}},
{:text, 'foo', %{column: 15, line: 1}},
{:middle_expr, '', ' end do ', %{column: 18, line: 1}},
{:text, 'bar', %{column: 30, line: 1}},
{:end_expr, '', ' end ', %{column: 33, line: 1}},
{:eof, %{column: 42, line: 1}}
]
assert EEx.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', @opts) == {:ok, exprs}
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) == {:ok, [
{:text, 'foo '},
{:start_expr, 1, '', ' cond do '},
{:middle_expr, 1, '', ' false -> '},
{:text, 'bar'},
{:middle_expr, 1, '', ' true -> '},
{:text, 'baz'},
{:end_expr, 1, '', ' end '}
]}
end
test "strings with embedded keywords blocks" do
exprs = [
{:text, 'foo ', %{column: 1, line: 1}},
{:start_expr, '', ' if true do ', %{column: 5, line: 1}},
{:text, 'bar', %{column: 21, line: 1}},
{:middle_expr, '', ' else ', %{column: 24, line: 1}},
{:text, 'baz', %{column: 34, line: 1}},
{:end_expr, '', ' end ', %{column: 37, line: 1}},
{:eof, %{column: 46, line: 1}}
]
assert EEx.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', @opts) ==
{:ok, exprs}
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == {:ok, [
{:text, 'foo '},
{:start_expr, 1, '', ' if true do '},
{:text, 'bar'},
{:middle_expr, 1, '', ' else '},
{:text, 'baz'},
{:end_expr, 1, '', ' end '}
]}
end
test "trim mode" do
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> \n\n '
exprs = [
{:start_expr, '=', ' if true do ', %{column: 2, line: 1}},
{:text, '\n TRUE \n', %{column: 20, line: 1}},
{:middle_expr, '', ' else ', %{column: 3, line: 3}},
{:text, '\n FALSE \n', %{column: 13, line: 3}},
{:end_expr, '', ' end ', %{column: 3, line: 5}},
{:eof, %{column: 3, line: 7}}
]
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> '
assert T.tokenize(template, 1, trim: true) == {:ok, [
{:start_expr, 1, '=', ' if true do '},
{:text, ' TRUE \n'},
{:middle_expr, 3, '', ' else '},
{:text, ' FALSE \n'},
{:end_expr, 5, '', ' end '}
]}
end
test "trim mode with comment" do
exprs = [
{:text, '\n123', %{column: 19, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(' <%# comment %> \n123', [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with multi-line comment" do
exprs = [
{:comment, ' comment ', %{column: 3, line: 1}},
{:text, '\n123', %{column: 23, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(' <%!-- comment --%> \n123', [trim: true] ++ @opts) == {:ok, exprs}
assert T.tokenize(' <%# comment %> \n123', 1, trim: true) == {:ok, [
{:text, '123'}
]}
end
test "trim mode with CRLF" do
exprs = [
{:text, '0\n', %{column: 1, line: 1}},
{:expr, '=', ' 12 ', %{column: 3, line: 2}},
{:text, '\n34', %{column: 15, line: 2}},
{:eof, %{column: 3, line: 3}}
]
assert EEx.tokenize('0\r\n <%= 12 %> \r\n34', [trim: true] ++ @opts) == {:ok, exprs}
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, trim: true) == {:ok, [
{:text, '0\r\n'},
{:expr, 2, '=', ' 12 '},
{:text, '34'}
]}
end
test "trim mode set to false" do
exprs = [
{:text, ' ', %{column: 1, line: 1}},
{:expr, '=', ' 12 ', %{column: 2, line: 1}},
{:text, ' \n', %{column: 11, line: 1}},
{:eof, %{column: 1, line: 2}}
]
assert EEx.tokenize(' <%= 12 %> \n', [trim: false] ++ @opts) == {:ok, exprs}
assert T.tokenize(' <%= 12 %> \n', 1, trim: false) == {:ok, [
{:text, ' '},
{:expr, 1, '=', ' 12 '},
{:text, ' \n'}
]}
end
test "trim mode no false positives" do
assert_not_trimmed = fn x ->
assert EEx.tokenize(x, [trim: false] ++ @opts) == EEx.tokenize(x, @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')
@@ -373,23 +156,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 EEx.tokenize('foo <% :bar', @opts) ==
{:error, "missing token '%>'", %{column: 12, line: 1}}
assert EEx.tokenize('<%# true ', @opts) ==
{:error, "missing token '%>'", %{column: 10, line: 1}}
assert EEx.tokenize('<%!-- foo ', @opts) ==
{:error, "missing token '--%>'", %{column: 11, line: 1}}
end
test "marks invalid expressions as regular expressions" do
assert EEx.tokenize('<% 1 $ 2 %>', @opts) ==
{:ok,
[
{:expr, [], ' 1 $ 2 ', %{column: 1, line: 1}},
{:eof, %{column: 12, line: 1}}
]}
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
+289 -650
View File
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
foo <%= bar
+1 -8
View File
@@ -1,8 +1 @@
{line_exclude, line_include} =
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
ExUnit.start(
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
)
ExUnit.start [trace: "--trace" in System.argv]
-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
-118
View File
@@ -1,118 +0,0 @@
# Returns config for Elixir docs
canonical = System.fetch_env!("CANONICAL")
[
extras: Path.wildcard("lib/elixir/pages/*.md") ++ ["CHANGELOG.md"],
deps: [
eex: "https://hexdocs.pm/eex/#{canonical}",
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
iex: "https://hexdocs.pm/iex/#{canonical}",
logger: "https://hexdocs.pm/logger/#{canonical}",
mix: "https://hexdocs.pm/mix/#{canonical}"
],
groups_for_functions: [
Guards: &(&1[:guard] == true)
],
skip_undefined_reference_warnings_on: ["lib/elixir/pages/compatibility-and-deprecations.md"],
groups_for_modules: [
# [Kernel, Kernel.SpecialForms],
"Basic Types": [
Atom,
Base,
Bitwise,
Date,
DateTime,
Exception,
Float,
Function,
Integer,
Module,
NaiveDateTime,
Record,
Regex,
String,
Time,
Tuple,
URI,
Version,
Version.Requirement
],
"Collections & Enumerables": [
Access,
Date.Range,
Enum,
Keyword,
List,
Map,
MapSet,
Range,
Stream
],
"IO & System": [
File,
File.Stat,
File.Stream,
IO,
IO.ANSI,
IO.Stream,
OptionParser,
Path,
Port,
StringIO,
System
],
Calendar: [
Calendar,
Calendar.ISO,
Calendar.TimeZoneDatabase,
Calendar.UTCOnlyTimeZoneDatabase
],
"Processes & Applications": [
Agent,
Application,
Config,
Config.Provider,
Config.Reader,
DynamicSupervisor,
GenServer,
Node,
PartitionSupervisor,
Process,
Registry,
Supervisor,
Task,
Task.Supervisor
],
Protocols: [
Collectable,
Enumerable,
Inspect,
Inspect.Algebra,
Inspect.Opts,
List.Chars,
Protocol,
String.Chars
],
"Code & Macros": [
Code,
Code.Fragment,
Kernel.ParallelCompiler,
Macro,
Macro.Env
]
## Automatically detected groups
# Deprecated: [
# Behaviour,
# Dict,
# GenEvent,
# HashDict,
# HashSet,
# Set,
# Supervisor.Spec
# ]
]
]
-13
View File
@@ -1,13 +0,0 @@
#!/usr/bin/env escript
%% -*- erlang -*-
main([Source, Target, Version]) ->
{ok, [{application, Name, Props0}]} = file:consult(Source),
Ebin = filename:dirname(Target),
Files = filelib:wildcard(filename:join(Ebin, "*.beam")),
Mods = [list_to_atom(filename:basename(F, ".beam")) || F <- Files],
Props1 = lists:keyreplace(modules, 1, Props0, {modules, Mods}),
Props = lists:keyreplace(vsn, 1, Props1, {vsn, Version}),
AppDef = io_lib:format("~tp.~n", [{application, Name, Props}]),
ok = file:write_file(Target, AppDef),
io:format("Generated ~ts app~n", [Name]).
+193 -641
View File
File diff suppressed because it is too large Load Diff
+90 -286
View File
@@ -6,55 +6,48 @@ defmodule Agent do
must be accessed from different processes or by the same process
at different points in time.
The `Agent` module provides a basic server implementation that
The Agent module provides a basic server implementation that
allows state to be retrieved and updated via a simple API.
## Examples
For example, the following agent implements a counter:
For example, in the Mix tool that ships with Elixir, we need
to keep a set of all tasks executed by a given project. Since
this set is shared, we can implement it with an Agent:
defmodule Counter do
use Agent
def start_link(initial_value) do
Agent.start_link(fn -> initial_value end, name: __MODULE__)
defmodule Mix.TasksServer do
def start_link do
Agent.start_link(fn -> MapSet.new end, name: __MODULE__)
end
def value do
Agent.get(__MODULE__, & &1)
@doc "Checks if the task has already executed"
def executed?(task, project) do
item = {task, project}
Agent.get(__MODULE__, fn set ->
item in set
end)
end
def increment do
Agent.update(__MODULE__, &(&1 + 1))
@doc "Marks a task as executed"
def put_task(task, project) do
item = {task, project}
Agent.update(__MODULE__, &MapSet.put(&1, item))
end
@doc "Resets the executed tasks and returns the previous list of tasks"
def take_all() do
Agent.get_and_update(__MODULE__, fn set ->
{Enum.into(set, []), MapSet.new}
end)
end
end
Usage would be:
Counter.start_link(0)
#=> {:ok, #PID<0.123.0>}
Counter.value()
#=> 0
Counter.increment()
#=> :ok
Counter.increment()
#=> :ok
Counter.value()
#=> 2
Thanks to the agent server process, the counter can be safely incremented
concurrently.
Agents provide a segregation between the client and server APIs (similar to
`GenServer`s). In particular, the functions passed as arguments to the calls to
`Agent` functions are invoked inside the agent (the server). This distinction
is important because you may want to avoid expensive operations inside the
agent, as they will effectively block the agent until the request is
fulfilled.
Note that agents still provide a segregation between the
client and server APIs, as seen in GenServers. In particular,
all code inside the function passed to the agent is executed
by the agent. This distinction is important because you may
want to avoid expensive operations inside the agent, as it will
effectively block the agent until the request is fulfilled.
Consider these two examples:
@@ -65,79 +58,19 @@ defmodule Agent do
# Compute in the agent/client
def get_something(agent) do
Agent.get(agent, & &1) |> do_something_expensive()
Agent.get(agent, &(&1)) |> do_something_expensive()
end
The first function blocks the agent. The second function copies all the state
to the client and then executes the operation in the client. One aspect to
consider is whether the data is large enough to require processing in the server,
at least initially, or small enough to be sent to the client cheaply. Another
factor is whether the data needs to be processed atomically: getting the
state and calling `do_something_expensive(state)` outside of the agent means
that the agent's state can be updated in the meantime. This is specially
important in case of updates as computing the new state in the client rather
than in the server can lead to race conditions if multiple clients are trying
to update the same state to different values.
The first function blocks the agent. The second function copies
all the state to the client and then executes the operation in the
client. The difference is whether the data is large enough to require
processing in the server, at least initially, or small enough to be
sent to the client cheaply.
## How to supervise
## Name Registration
An `Agent` is most commonly started under a supervision tree.
When we invoke `use Agent`, it automatically defines a `child_spec/1`
function that allows us to start the agent directly under a supervisor.
To start an agent under a supervisor with an initial counter of 0,
one may do:
children = [
{Counter, 0}
]
Supervisor.start_link(children, strategy: :one_for_all)
While one could also simply pass the `Counter` as a child to the supervisor,
such as:
children = [
Counter # Same as {Counter, []}
]
Supervisor.start_link(children, strategy: :one_for_all)
The definition above wouldn't work for this particular example,
as it would attempt to start the counter with an initial value
of an empty list. However, this may be a viable option in your
own agents. A common approach is to use a keyword list, as that
would allow setting the initial value and giving a name to the
counter process, for example:
def start_link(opts) do
{initial_value, opts} = Keyword.pop(opts, :initial_value, 0)
Agent.start_link(fn -> initial_value end, opts)
end
and then you can use `Counter`, `{Counter, name: :my_counter}` or
even `{Counter, initial_value: 0, name: :my_counter}` as a child
specification.
`use Agent` also accepts a list of options which configures the
child specification and therefore how it runs under a supervisor.
The generated `child_spec/1` can be customized with the following options:
* `:id` - the child specification identifier, defaults to the current module
* `:restart` - when the child should be restarted, defaults to `:permanent`
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
For example:
use Agent, restart: :transient, shutdown: 10_000
See the "Child specification" section in the `Supervisor` module for more
detailed information. The `@doc` annotation immediately preceding
`use Agent` will be attached to the generated `child_spec/1` function.
## Name registration
An agent is bound to the same name registration rules as GenServers.
Read more about it in the `GenServer` documentation.
An Agent is bound to the same name registration rules as GenServers.
Read more about it in the `GenServer` docs.
## A word on distributed agents
@@ -162,15 +95,14 @@ defmodule Agent do
## Hot code swapping
An agent can have its code hot swapped live by simply passing a module,
function, and arguments tuple to the update instruction. For example, imagine
function, and args tuple to the update instruction. For example, imagine
you have an agent named `:sample` and you want to convert its inner state
from a keyword list to a map. It can be done with the following
from some dict structure to a map. It can be done with the following
instruction:
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
The agent's state will be added to the given list of arguments (`[%{}]`) as
the first argument.
The agent's state will be added to the given list as the first argument.
"""
@typedoc "Return values of `start*` functions"
@@ -185,51 +117,14 @@ defmodule Agent do
@typedoc "The agent state"
@type state :: term
@doc """
Returns a specification to start an agent under a supervisor.
See the "Child specification" section in the `Supervisor` module for more detailed information.
"""
@doc since: "1.5.0"
def child_spec(arg) do
%{
id: Agent,
start: {Agent, :start_link, [arg]}
}
end
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
See `Supervisor`.
"""
end
def child_spec(arg) do
default = %{
id: __MODULE__,
start: {__MODULE__, :start_link, [arg]}
}
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
end
defoverridable child_spec: 1
end
end
@doc """
Starts an agent linked to the current process with the given function.
This is often used to start the agent as part of a supervision tree.
Once the agent is spawned, the given function `fun` is invoked in the server
process, and should return the initial agent state. Note that `start_link/2`
does not return until the given function has returned.
Once the agent is spawned, the given function is invoked and its return
value is used as the agent state. Note that `start_link` does not return
until the given function has returned.
## Options
@@ -241,7 +136,7 @@ defmodule Agent do
and the start function will return `{:error, :timeout}`.
If the `:debug` option is present, the corresponding function in the
[`:sys` module](`:sys`) will be invoked.
[`:sys` module](http://www.erlang.org/doc/man/sys.html) will be invoked.
If the `:spawn_opt` option is present, its value will be passed as options
to the underlying process as in `Process.spawn/4`.
@@ -249,36 +144,26 @@ defmodule Agent do
## Return values
If the server is successfully created and initialized, the function returns
`{:ok, pid}`, where `pid` is the PID of the server. If an agent with the
`{:ok, pid}`, where `pid` is the pid of the server. If an agent with the
specified name already exists, the function returns
`{:error, {:already_started, pid}}` with the PID of that process.
If the given function callback fails, the function returns `{:error, reason}`.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
iex> {:error, {exception, _stacktrace}} = Agent.start(fn -> raise "oops" end)
iex> exception
%RuntimeError{message: "oops"}
`{:error, {:already_started, pid}}` with the pid of that process.
If the given function callback fails with `reason`, the function returns
`{:error, reason}`.
"""
@spec start_link((() -> term), GenServer.options()) :: on_start
@spec start_link((() -> term), GenServer.options) :: on_start
def start_link(fun, options \\ []) when is_function(fun, 0) do
GenServer.start_link(Agent.Server, fun, options)
end
@doc """
Starts an agent linked to the current process.
Starts an agent linked to the current process with the given module
function and arguments.
Same as `start_link/2` but a module, function, and arguments are expected
instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state.
Same as `start_link/2` but a module, function and args are expected
instead of an anonymous function.
"""
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
@spec start_link(module, atom, [any], GenServer.options) :: on_start
def start_link(module, fun, args, options \\ []) do
GenServer.start_link(Agent.Server, {module, fun, args}, options)
end
@@ -287,48 +172,31 @@ defmodule Agent do
Starts an agent process without links (outside of a supervision tree).
See `start_link/2` for more information.
## Examples
iex> {:ok, pid} = Agent.start(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
"""
@spec start((() -> term), GenServer.options()) :: on_start
@spec start((() -> term), GenServer.options) :: on_start
def start(fun, options \\ []) when is_function(fun, 0) do
GenServer.start(Agent.Server, fun, options)
end
@doc """
Starts an agent without links with the given module, function, and arguments.
Starts an agent with the given module function and arguments.
See `start_link/4` for more information.
Similar to `start/2` but a module, function and args are expected
instead of an anonymous function.
"""
@spec start(module, atom, [any], GenServer.options()) :: on_start
@spec start(module, atom, [any], GenServer.options) :: on_start
def start(module, fun, args, options \\ []) do
GenServer.start(Agent.Server, {module, fun, args}, options)
end
@doc """
Gets an agent value via the given anonymous function.
Gets an agent value via the given function.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The result of the function invocation is
returned from this function.
`timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
exits.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
returned.
A timeout can also be specified (it has a default value of 5000).
"""
@spec get(agent, (state -> a), timeout) :: a when a: var
def get(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
@@ -338,9 +206,9 @@ defmodule Agent do
@doc """
Gets an agent value via the given function.
Same as `get/3` but a module, function, and arguments are expected
Same as `get/3` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
argument to the given list of args.
"""
@spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do
@@ -348,28 +216,14 @@ defmodule Agent do
end
@doc """
Gets and updates the agent state in one operation via the given anonymous
function.
Gets and updates the agent state in one operation.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The function must return a tuple with two
elements, the first being the value to return (that is, the "get" value)
and the second one being the new state of the agent.
`timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
exits.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get_and_update(pid, fn state -> {state, state + 1} end)
42
iex> Agent.get(pid, fn state -> state end)
43
elements, the first being the value to return (i.e. the `get` value)
and the second one is the new state.
A timeout can also be specified (it has a default value of 5000).
"""
@spec get_and_update(agent, (state -> {a, state}), timeout) :: a when a: var
def get_and_update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
@@ -377,11 +231,11 @@ defmodule Agent do
end
@doc """
Gets and updates the agent state in one operation via the given function.
Gets and updates the agent state in one operation.
Same as `get_and_update/3` but a module, function, and arguments are expected
Same as `get_and_update/3` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
argument to the given list of args.
"""
@spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
@@ -389,28 +243,13 @@ defmodule Agent do
end
@doc """
Updates the agent state via the given anonymous function.
Updates the agent state.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The return value of `fun` becomes the new
state of the agent.
passing the agent state. The function must return the new state.
A timeout can also be specified (it has a default value of 5000).
This function always returns `:ok`.
`timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
exits.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43
"""
@spec update(agent, (state -> state), timeout) :: :ok
def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
@@ -418,20 +257,11 @@ defmodule Agent do
end
@doc """
Updates the agent state via the given function.
Updates the agent state.
Same as `update/3` but a module, function, and arguments are expected
Same as `update/3` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, Kernel, :+, [12])
:ok
iex> Agent.get(pid, fn state -> state end)
54
argument to the given list of args.
"""
@spec update(agent, module, atom, [term], timeout) :: :ok
def update(agent, module, fun, args, timeout \\ 5000) do
@@ -439,23 +269,13 @@ defmodule Agent do
end
@doc """
Performs a cast (*fire and forget*) operation on the agent state.
Performs a cast (fire and forget) operation on the agent state.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The return value of `fun` becomes the new
state of the agent.
Note that `cast` returns `:ok` immediately, regardless of whether `agent` (or
the node it should live on) exists.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.cast(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43
passing the agent state. The function must return the new state.
Note that `cast` returns `:ok` immediately, regardless of whether the
destination node or agent exists.
"""
@spec cast(agent, (state -> state)) :: :ok
def cast(agent, fun) when is_function(fun, 1) do
@@ -463,20 +283,11 @@ defmodule Agent do
end
@doc """
Performs a cast (*fire and forget*) operation on the agent state.
Performs a cast (fire and forget) operation on the agent state.
Same as `cast/2` but a module, function, and arguments are expected
Same as `cast/2` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.cast(pid, Kernel, :+, [12])
:ok
iex> Agent.get(pid, fn state -> state end)
54
argument to the given list of args.
"""
@spec cast(agent, module, atom, [term]) :: :ok
def cast(agent, module, fun, args) do
@@ -484,25 +295,18 @@ defmodule Agent do
end
@doc """
Synchronously stops the agent with the given `reason`.
Stops the agent with the given `reason`.
It returns `:ok` if the agent terminates with the given
reason. If the agent terminates with another reason, the call will
It returns `:ok` if the server terminates with the given
reason, if it terminates with another reason, the call will
exit.
This function keeps OTP semantics regarding error reporting.
If the reason is any other than `:normal`, `:shutdown` or
`{:shutdown, _}`, an error report will be logged.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.stop(pid)
:ok
"""
@spec stop(agent, reason :: term, timeout) :: :ok
def stop(agent, reason \\ :normal, timeout \\ :infinity) do
GenServer.stop(agent, reason, timeout)
:gen.stop(agent, reason, timeout)
end
end
+11 -3
View File
@@ -15,7 +15,7 @@ defmodule Agent.Server do
def handle_call({:get_and_update, fun}, _from, state) do
case run(fun, [state]) do
{reply, state} -> {:reply, reply, state}
other -> {:stop, {:bad_return_value, other}, state}
other -> {:stop, {:bad_return_value, other}, state}
end
end
@@ -23,10 +23,18 @@ defmodule Agent.Server do
{:reply, :ok, run(fun, [state])}
end
def handle_call(msg, from, state) do
super(msg, from, state)
end
def handle_cast({:cast, fun}, state) do
{:noreply, run(fun, [state])}
end
def handle_cast(msg, state) do
super(msg, state)
end
def code_change(_old, state, fun) do
{:ok, run(fun, [state])}
end
@@ -37,8 +45,8 @@ defmodule Agent.Server do
end
defp get_initial_call(fun) when is_function(fun, 0) do
{:module, module} = Function.info(fun, :module)
{:name, name} = Function.info(fun, :name)
{:module, module} = :erlang.fun_info(fun, :module)
{:name, name} = :erlang.fun_info(fun, :name)
{module, name, 0}
end
File diff suppressed because it is too large Load Diff
+5 -43
View File
@@ -1,46 +1,8 @@
defmodule Atom do
@moduledoc """
Atoms are constants whose values are their own name.
They are often useful to enumerate over distinct values, such as:
iex> :apple
:apple
iex> :orange
:orange
iex> :watermelon
:watermelon
Atoms are equal if their names are equal.
iex> :apple == :apple
true
iex> :apple == :orange
false
Often they are used to express the state of an operation, by using
values such as `:ok` and `:error`.
The booleans `true` and `false` are also atoms:
iex> true == :true
true
iex> is_atom(false)
true
iex> is_boolean(:false)
true
Elixir allows you to skip the leading `:` for the atoms `false`, `true`,
and `nil`.
Atoms must be composed of Unicode characters such as letters, numbers,
underscore, and `@`. If the keyword has a character that does not
belong to the category above, such as spaces, you can wrap it in
quotes:
iex> :"this is an atom with spaces"
:"this is an atom with spaces"
Convenience functions for working with atoms.
See also `Kernel.is_atom/1`.
"""
@doc """
@@ -54,9 +16,9 @@ defmodule Atom do
"foo"
"""
@spec to_string(atom) :: String.t()
@spec to_string(atom) :: String.t
def to_string(atom) do
:erlang.atom_to_binary(atom)
:erlang.atom_to_binary(atom, :utf8)
end
@doc """
@@ -75,8 +37,8 @@ defmodule Atom do
:erlang.atom_to_list(atom)
end
# TODO: Deprecate by v1.5
@doc false
@deprecated "Use Atom.to_charlist/1 instead"
@spec to_char_list(atom) :: charlist
def to_char_list(atom), do: Atom.to_charlist(atom)
end
+477 -746
View File
File diff suppressed because it is too large Load Diff
+16 -40
View File
@@ -1,23 +1,17 @@
defmodule Behaviour do
@moduledoc """
Mechanism for handling behaviours.
This module has been deprecated.
This module is deprecated. Instead of `defcallback/1` and
`defmacrocallback/1`, the `@callback` and `@macrocallback`
module attributes can be used respectively. See the
documentation for `Module` for more information on these
attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used.
Instead of `defcallback`, one can simply use `@callback`.
Instead of `defmacrocallback`, one can simply use `@macrocallback`.
Instead of `__behaviour__(:callbacks)`, one can simply use `behaviour_info(:callbacks)`.
"""
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
# TODO: Deprecate by 1.4
@doc """
Defines a function callback according to the given type specification.
"""
@deprecated "Use the @callback module attribute instead"
defmacro defcallback(spec) do
do_defcallback(:def, split_spec(spec, quote(do: term)))
end
@@ -25,12 +19,11 @@ defmodule Behaviour do
@doc """
Defines a macro callback according to the given type specification.
"""
@deprecated "Use the @macrocallback module attribute instead"
defmacro defmacrocallback(spec) do
do_defcallback(:defmacro, split_spec(spec, quote(do: Macro.t())))
do_defcallback(:defmacro, split_spec(spec, quote(do: Macro.t)))
end
defp split_spec({:when, _, [{:"::", _, [spec, return]}, guard]}, _default) do
defp split_spec({:when, _, [{:::, _, [spec, return]}, guard]}, _default) do
{spec, return, guard}
end
@@ -38,7 +31,7 @@ defmodule Behaviour do
{spec, default, guard}
end
defp split_spec({:"::", _, [spec, return]}, _default) do
defp split_spec({:::, _, [spec, return]}, _default) do
{spec, return, []}
end
@@ -50,25 +43,21 @@ defmodule Behaviour do
case Macro.decompose_call(spec) do
{name, args} ->
do_callback(kind, name, args, return, guards)
_ ->
raise ArgumentError, "invalid syntax in #{kind}callback #{Macro.to_string(spec)}"
end
end
defp do_callback(kind, name, args, return, guards) do
fun = fn
{:"::", _, [left, right]} ->
:lists.foreach fn
{:::, _, [left, right]} ->
ensure_not_default(left)
ensure_not_default(right)
left
other ->
ensure_not_default(other)
other
end
:lists.foreach(fun, args)
end, args
spec =
quote do
@@ -76,8 +65,8 @@ defmodule Behaviour do
end
case kind do
:def -> quote(do: @callback(unquote(spec)))
:defmacro -> quote(do: @macrocallback(unquote(spec)))
:def -> quote(do: @callback unquote(spec))
:defmacro -> quote(do: @macrocallback unquote(spec))
end
end
@@ -90,33 +79,20 @@ defmodule Behaviour do
@doc false
defmacro __using__(_) do
quote do
warning =
"the Behaviour module is deprecated. Instead of using this module, " <>
"use the @callback and @macrocallback module attributes. See the " <>
"documentation for Module for more information on these attributes"
IO.warn(warning)
@doc false
def __behaviour__(:callbacks) do
__MODULE__.behaviour_info(:callbacks)
end
def __behaviour__(:docs) do
{:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(__MODULE__)
for {{kind, name, arity}, line, _, doc, _} <- docs, kind in [:callback, :macrocallback] do
for {tuple, line, kind, docs} <- Code.get_docs(__MODULE__, :callback_docs) do
case kind do
:callback -> {{name, arity}, line, :def, __behaviour__doc_value(doc)}
:macrocallback -> {{name, arity}, line, :defmacro, __behaviour__doc_value(doc)}
:callback -> {tuple, line, :def, docs}
:macrocallback -> {tuple, line, :defmacro, docs}
end
end
end
defp __behaviour__doc_value(%{"en" => doc}), do: doc
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(_), do: nil
import unquote(__MODULE__)
end
end
+79 -136
View File
@@ -1,40 +1,50 @@
defmodule Bitwise do
@moduledoc """
A set of functions that perform calculations on bits.
A set of macros that perform calculations on bits.
All bitwise functions work only on integers; otherwise an
`ArithmeticError` is raised. The functions `band/2`,
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
The macros in this module come in two flavors: named or
operators. For example:
## Guards
iex> use Bitwise
iex> bnot 1 # named
-2
iex> 1 &&& 1 # operator
1
All bitwise functions can be used in guards:
If you prefer to use only operators or skip them, you can
pass the following options:
iex> odd? = fn
...> int when Bitwise.band(int, 1) == 1 -> true
...> _ -> false
...> end
* `:only_operators` - include only operators
* `:skip_operators` - skip operators
For example:
iex> use Bitwise, only_operators: true
iex> 1 &&& 1
1
When invoked with no options, `use Bitwise` is equivalent
to `import Bitwise`.
All bitwise macros can be used in guards:
iex> use Bitwise
iex> odd? = fn int when band(int, 1) == 1 -> true; _ -> false end
iex> odd?.(1)
true
All functions in this module are inlined by the compiler.
"""
@doc false
@deprecated "import Bitwise instead"
defmacro __using__(options) do
except =
cond do
Keyword.get(options, :only_operators) ->
[bnot: 1, band: 2, bor: 2, bxor: 2, bsl: 2, bsr: 2]
Keyword.get(options, :skip_operators) ->
["~~~": 1, &&&: 2, |||: 2, "^^^": 2, <<<: 2, >>>: 2]
true ->
[]
end
except = cond do
Keyword.get(options, :only_operators) ->
[bnot: 1, band: 2, bor: 2, bxor: 2, bsl: 2, bsr: 2]
Keyword.get(options, :skip_operators) ->
[~~~: 1, &&&: 2, |||: 2, ^^^: 2, <<<: 2, >>>: 2]
true ->
[]
end
quote do
import Bitwise, except: unquote(except)
@@ -42,229 +52,162 @@ defmodule Bitwise do
end
@doc """
Calculates the bitwise NOT of the argument.
Allowed in guard tests. Inlined by the compiler.
## Examples
Calculates the bitwise NOT of its argument.
iex> bnot(2)
-3
iex> bnot(2) &&& 3
1
"""
@doc guard: true
@spec bnot(integer) :: integer
def bnot(expr) do
:erlang.bnot(expr)
defmacro bnot(expr) do
quote do: :erlang.bnot(unquote(expr))
end
@doc false
def unquote(:"~~~")(expr) do
:erlang.bnot(expr)
@doc """
Prefix (unary) operator; calculates the bitwise NOT of its argument.
iex> ~~~2
-3
iex> ~~~2 &&& 3
1
"""
defmacro ~~~expr do
quote do: :erlang.bnot(unquote(expr))
end
@doc """
Calculates the bitwise AND of its arguments.
Allowed in guard tests. Inlined by the compiler.
## Examples
iex> band(9, 3)
1
"""
@doc guard: true
@spec band(integer, integer) :: integer
def band(left, right) do
:erlang.band(left, right)
defmacro band(left, right) do
quote do: :erlang.band(unquote(left), unquote(right))
end
@doc """
Bitwise AND operator.
Calculates the bitwise AND of its arguments.
Allowed in guard tests. Inlined by the compiler.
## Examples
Infix operator; calculates the bitwise AND of its arguments.
iex> 9 &&& 3
1
"""
@doc guard: true
@spec integer &&& integer :: integer
def left &&& right do
:erlang.band(left, right)
defmacro left &&& right do
quote do: :erlang.band(unquote(left), unquote(right))
end
@doc """
Calculates the bitwise OR of its arguments.
Allowed in guard tests. Inlined by the compiler.
## Examples
iex> bor(9, 3)
11
"""
@doc guard: true
@spec bor(integer, integer) :: integer
def bor(left, right) do
:erlang.bor(left, right)
defmacro bor(left, right) do
quote do: :erlang.bor(unquote(left), unquote(right))
end
@doc """
Bitwise OR operator.
Calculates the bitwise OR of its arguments.
Allowed in guard tests. Inlined by the compiler.
## Examples
Infix operator; calculates the bitwise OR of its arguments.
iex> 9 ||| 3
11
"""
@doc guard: true
@spec integer ||| integer :: integer
def left ||| right do
:erlang.bor(left, right)
defmacro left ||| right do
quote do: :erlang.bor(unquote(left), unquote(right))
end
@doc """
Calculates the bitwise XOR of its arguments.
Allowed in guard tests. Inlined by the compiler.
## Examples
iex> bxor(9, 3)
10
"""
@doc guard: true
@spec bxor(integer, integer) :: integer
def bxor(left, right) do
:erlang.bxor(left, right)
defmacro bxor(left, right) do
quote do: :erlang.bxor(unquote(left), unquote(right))
end
@doc false
def unquote(:"^^^")(left, right) do
:erlang.bxor(left, right)
@doc """
Infix operator; calculates the bitwise XOR of its arguments.
iex> 9 ^^^ 3
10
"""
defmacro left ^^^ right do
quote do: :erlang.bxor(unquote(left), unquote(right))
end
@doc """
Calculates the result of an arithmetic left bitshift.
Allowed in guard tests. Inlined by the compiler.
## Examples
iex> bsl(1, 2)
4
iex> bsl(1, -2)
0
iex> bsl(-1, 2)
-4
iex> bsl(-1, -2)
-1
"""
@doc guard: true
@spec bsl(integer, integer) :: integer
def bsl(left, right) do
:erlang.bsl(left, right)
defmacro bsl(left, right) do
quote do: :erlang.bsl(unquote(left), unquote(right))
end
@doc """
Arithmetic left bitshift operator.
Calculates the result of an arithmetic left bitshift.
Allowed in guard tests. Inlined by the compiler.
## Examples
Infix operator; calculates the result of an arithmetic left bitshift.
iex> 1 <<< 2
4
iex> 1 <<< -2
0
iex> -1 <<< 2
-4
iex> -1 <<< -2
-1
"""
@doc guard: true
@spec integer <<< integer :: integer
def left <<< right do
:erlang.bsl(left, right)
defmacro left <<< right do
quote do: :erlang.bsl(unquote(left), unquote(right))
end
@doc """
Calculates the result of an arithmetic right bitshift.
Allowed in guard tests. Inlined by the compiler.
## Examples
iex> bsr(1, 2)
0
iex> bsr(1, -2)
4
iex> bsr(-1, 2)
-1
iex> bsr(-1, -2)
-4
"""
@doc guard: true
@spec bsr(integer, integer) :: integer
def bsr(left, right) do
:erlang.bsr(left, right)
defmacro bsr(left, right) do
quote do: :erlang.bsr(unquote(left), unquote(right))
end
@doc """
Arithmetic right bitshift operator.
Calculates the result of an arithmetic right bitshift.
Allowed in guard tests. Inlined by the compiler.
## Examples
Infix operator; calculates the result of an arithmetic right bitshift.
iex> 1 >>> 2
0
iex> 1 >>> -2
4
iex> -1 >>> 2
-1
iex> -1 >>> -2
-4
"""
@doc guard: true
@spec integer >>> integer :: integer
def left >>> right do
:erlang.bsr(left, right)
defmacro left >>> right do
quote do: :erlang.bsr(unquote(left), unquote(right))
end
end
+992 -668
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-227
View File
@@ -1,227 +0,0 @@
defmodule Date.Range do
@moduledoc """
Returns an inclusive range between dates.
Ranges must be created with the `Date.range/2` or `Date.range/3` function.
The following fields are public:
* `:first` - the initial date on the range
* `:last` - the last date on the range
* `:step` - (since v1.12.0) the step
The remaining fields are private and should not be accessed.
"""
@type t :: %__MODULE__{
first: Date.t(),
last: Date.t(),
first_in_iso_days: days(),
last_in_iso_days: days(),
step: pos_integer | neg_integer
}
@typep days() :: integer()
@enforce_keys [:first, :last, :first_in_iso_days, :last_in_iso_days, :step]
defstruct [:first, :last, :first_in_iso_days, :last_in_iso_days, :step]
defimpl Enumerable do
def member?(
%Date.Range{
first: %{calendar: calendar},
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
} = range,
%Date{calendar: calendar} = date
) do
{days, _} = Date.to_iso_days(date)
cond do
empty?(range) ->
{:ok, false}
first_days <= last_days ->
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
true ->
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
end
end
def member?(%Date.Range{step: _}, _) do
{:ok, false}
end
# TODO: Remove me on v2.0
def member?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range,
date
) do
step = if first_days <= last_days, do: 1, else: -1
member?(Map.put(date_range, :step, step), date)
end
def count(range) do
{:ok, size(range)}
end
def slice(
%Date.Range{
first_in_iso_days: first,
first: %{calendar: calendar},
step: step
} = range
) do
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
end
# TODO: Remove me on v2.0
def slice(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
slice(Map.put(date_range, :step, step))
end
defp slice(current, _step, 1, calendar) do
[date_from_iso_days(current, calendar)]
end
defp slice(current, step, remaining, calendar) do
[
date_from_iso_days(current, calendar)
| slice(current + step, step, remaining - 1, calendar)
]
end
def reduce(
%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
first: %{calendar: calendar},
step: step
},
acc,
fun
) do
reduce(first_days, last_days, acc, fun, step, calendar)
end
# TODO: Remove me on v2.0
def reduce(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range,
acc,
fun
) do
step = if first_days <= last_days, do: 1, else: -1
reduce(Map.put(date_range, :step, step), acc, fun)
end
defp reduce(_first_days, _last_days, {:halt, acc}, _fun, _step, _calendar) do
{:halted, acc}
end
defp reduce(first_days, last_days, {:suspend, acc}, fun, step, calendar) do
{:suspended, acc, &reduce(first_days, last_days, &1, fun, step, calendar)}
end
defp reduce(first_days, last_days, {:cont, acc}, fun, step, calendar)
when step > 0 and first_days <= last_days
when step < 0 and first_days >= last_days do
reduce(
first_days + step,
last_days,
fun.(date_from_iso_days(first_days, calendar), acc),
fun,
step,
calendar
)
end
defp reduce(_, _, {:cont, acc}, _fun, _step, _calendar) do
{:done, acc}
end
defp date_from_iso_days(days, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
end
defp date_from_iso_days(days, calendar) do
{year, month, day, _, _, _, _} =
calendar.naive_datetime_from_iso_days({days, {0, 86_400_000_000}})
%Date{year: year, month: month, day: day, calendar: calendar}
end
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step})
when step > 0 and first_days > last_days,
do: 0
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step})
when step < 0 and first_days < last_days,
do: 0
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step}),
do: abs(div(last_days - first_days, step)) + 1
# TODO: Remove me on v2.0
defp size(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
size(Map.put(date_range, :step, step))
end
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step > 0 and first_days > last_days,
do: true
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step < 0 and first_days < last_days,
do: true
defp empty?(%Date.Range{step: _}), do: false
# TODO: Remove me on v2.0
defp empty?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
empty?(Map.put(date_range, :step, step))
end
end
defimpl Inspect do
import Kernel, except: [inspect: 2]
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
end
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
end
# TODO: Remove me on v2.0
def inspect(%{__struct__: Date.Range, first: first, last: last} = date_range, opts) do
step = if first <= last, do: 1, else: -1
inspect(Map.put(date_range, :step, step), opts)
end
end
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-841
View File
@@ -1,841 +0,0 @@
defmodule Time do
@moduledoc """
A Time struct and functions.
The Time struct contains the fields hour, minute, second and microseconds.
New times can be built with the `new/4` function or using the
`~T` (see `sigil_T/2`) sigil:
iex> ~T[23:00:07.001]
~T[23:00:07.001]
Both `new/4` and sigil return a struct where the time fields can
be accessed directly:
iex> time = ~T[23:00:07.001]
iex> time.hour
23
iex> time.microsecond
{1000, 3}
The functions on this module work with the `Time` struct as well
as any struct that contains the same fields as the `Time` struct,
such as `NaiveDateTime` and `DateTime`. Such functions expect
`t:Calendar.time/0` in their typespecs (instead of `t:t/0`).
Developers should avoid creating the Time structs directly
and instead rely on the functions provided by this module as well
as the ones in third-party calendar libraries.
## Comparing times
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `Time` struct fields. For proper comparison between
times, use the `compare/2` function. The existence of the `compare/2`
function in this module also allows using `Enum.min/2` and `Enum.max/2`
functions to get the minimum and maximum time of an `Enum`. For example:
iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time)
~T[10:00:07.001]
"""
@enforce_keys [:hour, :minute, :second]
defstruct [:hour, :minute, :second, microsecond: {0, 0}, calendar: Calendar.ISO]
@type t :: %__MODULE__{
hour: Calendar.hour(),
minute: Calendar.minute(),
second: Calendar.second(),
microsecond: Calendar.microsecond(),
calendar: Calendar.calendar()
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current time in UTC.
## Examples
iex> time = Time.utc_now()
iex> time.hour >= 0
true
"""
@doc since: "1.4.0"
@spec utc_now(Calendar.calendar()) :: t
def utc_now(calendar \\ Calendar.ISO) do
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(:os.system_time(), :native)
{hour, minute, second} = time
iso_time = %Time{
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
convert!(iso_time, calendar)
end
@doc """
Builds a new time.
Expects all values to be integers. Returns `{:ok, time}` if each
entry fits its appropriate range, returns `{:error, reason}` otherwise.
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)
{:ok, ~T[00:00:00.000000]}
iex> Time.new(23, 59, 59, 999_999)
{:ok, ~T[23:59:59.999999]}
iex> Time.new(24, 59, 59, 999_999)
{:error, :invalid_time}
iex> Time.new(23, 60, 59, 999_999)
{:error, :invalid_time}
iex> Time.new(23, 59, 60, 999_999)
{:error, :invalid_time}
iex> Time.new(23, 59, 59, 1_000_000)
{:error, :invalid_time}
# Invalid precision
Time.new(23, 59, 59, {999_999, 10})
{:error, :invalid_time}
"""
@spec new(
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
def new(hour, minute, second, microsecond, calendar) when is_integer(microsecond) do
new(hour, minute, second, {microsecond, 6}, calendar)
end
def new(hour, minute, second, {microsecond, precision}, calendar)
when is_integer(hour) and is_integer(minute) and is_integer(second) and
is_integer(microsecond) and is_integer(precision) do
case calendar.valid_time?(hour, minute, second, {microsecond, precision}) do
true ->
time = %Time{
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
calendar: calendar
}
{:ok, time}
false ->
{:error, :invalid_time}
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.
### Examples
iex> Time.to_string(~T[23:00:00])
"23:00:00"
iex> Time.to_string(~T[23:00:00.001])
"23:00:00.001"
iex> Time.to_string(~T[23:00:00.123456])
"23:00:00.123456"
iex> Time.to_string(~N[2015-01-01 23:00:00.001])
"23:00:00.001"
iex> Time.to_string(~N[2015-01-01 23:00:00.123456])
"23:00:00.123456"
"""
@spec to_string(Calendar.time()) :: String.t()
def to_string(time)
def to_string(%{
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: calendar
}) do
calendar.time_to_string(hour, minute, second, microsecond)
end
@doc """
Parses the extended "Local time" format described by
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
Time zone offset may be included in the string but they will be
simply discarded as such information is not included in times.
As specified in the standard, the separator "T" may be omitted if
desired as there is no ambiguity within this function.
## Examples
iex> Time.from_iso8601("23:50:07")
{:ok, ~T[23:50:07]}
iex> Time.from_iso8601("23:50:07Z")
{:ok, ~T[23:50:07]}
iex> Time.from_iso8601("T23:50:07Z")
{:ok, ~T[23:50:07]}
iex> Time.from_iso8601("23:50:07,0123456")
{:ok, ~T[23:50:07.012345]}
iex> Time.from_iso8601("23:50:07.0123456")
{:ok, ~T[23:50:07.012345]}
iex> Time.from_iso8601("23:50:07.123Z")
{:ok, ~T[23:50:07.123]}
iex> Time.from_iso8601("2015:01:23 23-50-07")
{:error, :invalid_format}
iex> Time.from_iso8601("23:50:07A")
{:error, :invalid_format}
iex> Time.from_iso8601("23:50:07.")
{:error, :invalid_format}
iex> Time.from_iso8601("23:50:61")
{:error, :invalid_time}
"""
@spec from_iso8601(String.t(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
def from_iso8601(string, calendar \\ Calendar.ISO) do
with {:ok, {hour, minute, second, microsecond}} <- Calendar.ISO.parse_time(string) do
convert(
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond},
calendar
)
end
end
@doc """
Parses the extended "Local time" format described by
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
Raises if the format is invalid.
## Examples
iex> Time.from_iso8601!("23:50:07,123Z")
~T[23:50:07.123]
iex> Time.from_iso8601!("23:50:07.123Z")
~T[23:50:07.123]
iex> Time.from_iso8601!("2015:01:23 23-50-07")
** (ArgumentError) cannot parse "2015:01:23 23-50-07" as time, reason: :invalid_format
"""
@spec from_iso8601!(String.t(), Calendar.calendar()) :: t
def from_iso8601!(string, calendar \\ Calendar.ISO) do
case from_iso8601(string, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot parse #{inspect(string)} as time, reason: #{inspect(reason)}"
end
end
@doc """
Converts the given time to
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
By default, `Time.to_iso8601/2` returns times formatted in the "extended"
format, for human readability. It also supports the "basic" format through
passing the `:basic` option.
### Examples
iex> Time.to_iso8601(~T[23:00:13])
"23:00:13"
iex> Time.to_iso8601(~T[23:00:13.001])
"23:00:13.001"
iex> Time.to_iso8601(~T[23:00:13.001], :basic)
"230013.001"
iex> Time.to_iso8601(~N[2010-04-17 23:00:13])
"23:00:13"
"""
@spec to_iso8601(Calendar.time(), :extended | :basic) :: String.t()
def to_iso8601(time, format \\ :extended)
def to_iso8601(%{calendar: Calendar.ISO} = time, format) when format in [:extended, :basic] do
%{
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = time
Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
end
def to_iso8601(%{calendar: _} = time, format) when format in [:extended, :basic] do
time
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end
@doc """
Converts given `time` to an Erlang time tuple.
WARNING: Loss of precision may occur, as Erlang time tuples
only contain hours/minutes/seconds.
## Examples
iex> Time.to_erl(~T[23:30:15.999])
{23, 30, 15}
iex> Time.to_erl(~N[2010-04-17 23:30:15.999])
{23, 30, 15}
"""
@spec to_erl(Calendar.time()) :: :calendar.time()
def to_erl(time) do
%{hour: hour, minute: minute, second: second} = convert!(time, Calendar.ISO)
{hour, minute, second}
end
@doc """
Converts an Erlang time tuple to a `Time` struct.
## Examples
iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time}
"""
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
def from_erl({hour, minute, second}, microsecond, calendar) do
with {:ok, time} <- new(hour, minute, second, microsecond, Calendar.ISO),
do: convert(time, calendar)
end
@doc """
Converts an Erlang time tuple to a `Time` struct.
## Examples
iex> Time.from_erl!({23, 30, 15})
~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15})
** (ArgumentError) cannot convert {24, 30, 15} to time, reason: :invalid_time
"""
@spec from_erl!(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(tuple)} to time, reason: #{inspect(reason)}"
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 `amount_to_add` of `unit`s to the given `time`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar.
## Examples
iex> Time.add(~T[10:00:00], 27000)
~T[17:30:00]
iex> Time.add(~T[11:00:00.005], 2400)
~T[11:40:00.005]
iex> Time.add(~T[00:00:00.000], 86_399_999, :millisecond)
~T[23:59:59.999]
Negative values are allowed:
iex> Time.add(~T[23:00:00], -60)
~T[22:59:00]
Note that the time is cyclic:
iex> Time.add(~T[17:10:05], 86400)
~T[17:10:05]
Hours and minutes are also supported:
iex> Time.add(~T[17:10:05], 2, :hour)
~T[19:10:05]
iex> Time.add(~T[17:10:05], 30, :minute)
~T[17:40:05]
This operation merges the precision of the time with the given unit:
iex> result = Time.add(~T[00:29:10], 21, :millisecond)
~T[00:29:10.021]
iex> result.microsecond
{21000, 3}
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
def add(time, amount_to_add, unit \\ :second)
def add(time, amount_to_add, :hour) when is_integer(amount_to_add) do
add(time, amount_to_add * 3600, :second)
end
def add(time, amount_to_add, :minute) when is_integer(amount_to_add) do
add(time, amount_to_add * 60, :second)
end
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
when is_integer(amount_to_add) do
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
total = time_to_microseconds(time) + amount_to_add
parts = Integer.mod(total, @parts_per_day)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
%Time{
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
calendar: calendar
}
end
defp time_to_microseconds(%{
calendar: Calendar.ISO,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _}
}) do
0
end
defp time_to_microseconds(time) do
iso_days = {0, to_day_fraction(time)}
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
end
@doc """
Compares two time structs.
Returns `:gt` if first time is later than the second
and `:lt` for vice versa. If the two times are equal
`:eq` is returned.
## Examples
iex> Time.compare(~T[16:04:16], ~T[16:04:28])
:lt
iex> Time.compare(~T[16:04:16], ~T[16:04:16])
:eq
iex> Time.compare(~T[16:04:16.01], ~T[16:04:16.001])
:gt
This function can also be used to compare across more
complex calendar types by considering only the time fields:
iex> Time.compare(~N[1900-01-01 16:04:16], ~N[2015-01-01 16:04:16])
:eq
iex> Time.compare(~N[2015-01-01 16:04:16], ~N[2015-01-01 16:04:28])
:lt
iex> Time.compare(~N[2015-01-01 16:04:16.01], ~N[2000-01-01 16:04:16.001])
:gt
"""
@doc since: "1.4.0"
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
end
def compare(time1, time2) do
{parts1, ppd1} = to_day_fraction(time1)
{parts2, ppd2} = to_day_fraction(time2)
case {parts1 * ppd2, parts2 * ppd1} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
end
@doc """
Converts given `time` to a different calendar.
Returns `{:ok, time}` if the conversion was successful,
or `{:error, reason}` if it was not, for some reason.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
{:ok, %Time{calendar: Calendar.Holocene, hour: 13, minute: 30, second: 15, microsecond: {0, 0}}}
"""
@doc since: "1.5.0"
@spec convert(Calendar.time(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
# Keep it multiline for proper function clause errors.
def convert(
%{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
},
calendar
) do
time = %Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
{:ok, time}
end
def convert(%{microsecond: {_, precision}} = time, calendar) do
{hour, minute, second, {microsecond, _}} =
time
|> to_day_fraction()
|> calendar.time_from_day_fraction()
time = %Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}
{:ok, time}
end
@doc """
Similar to `Time.convert/2`, but raises an `ArgumentError`
if the conversion between the two calendars is not possible.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
%Time{calendar: Calendar.Holocene, hour: 13, minute: 30, second: 15, microsecond: {0, 0}}
"""
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
end
@doc """
Returns the difference between two times, considering only the hour, minute,
second and microsecond.
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
additional information about a date or time zone is ignored when calculating
the difference.
The answer can be returned in any `:hour`, `:minute`, `:second` or any
subsecond `unit` available from `t:System.time_unit/0`. If the first time
value is earlier than the second, a negative number is returned.
The unit is measured according to `Calendar.ISO` and defaults to `:second`.
Fractional results are not supported and are truncated.
## Examples
iex> Time.diff(~T[00:29:12], ~T[00:29:10])
2
# When passing a `NaiveDateTime` the date part is ignored.
iex> Time.diff(~N[2017-01-01 00:29:12], ~T[00:29:10])
2
# Two `NaiveDateTime` structs could have big differences in the date
# but only the time part is considered.
iex> Time.diff(~N[2017-01-01 00:29:12], ~N[1900-02-03 00:29:10])
2
iex> Time.diff(~T[00:29:12], ~T[00:29:10], :microsecond)
2_000_000
iex> Time.diff(~T[00:29:10], ~T[00:29:12], :microsecond)
-2_000_000
iex> Time.diff(~T[02:29:10], ~T[00:29:10], :hour)
2
iex> Time.diff(~T[02:29:10], ~T[00:29:11], :hour)
1
"""
@doc since: "1.5.0"
@spec diff(Calendar.time(), Calendar.time(), :hour | :minute | System.time_unit()) :: integer
def diff(time1, time2, unit \\ :second)
def diff(time1, time2, :hour) do
diff(time1, time2, :second) |> div(3600)
end
def diff(time1, time2, :minute) do
diff(time1, time2, :second) |> div(60)
end
def diff(
%{
calendar: Calendar.ISO,
hour: hour1,
minute: minute1,
second: second1,
microsecond: {microsecond1, _}
},
%{
calendar: Calendar.ISO,
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, _}
},
unit
) do
total =
(hour1 - hour2) * 3_600_000_000 + (minute1 - minute2) * 60_000_000 +
(second1 - second2) * 1_000_000 + (microsecond1 - microsecond2)
System.convert_time_unit(total, :microsecond, unit)
end
def diff(time1, time2, unit) do
fraction1 = to_day_fraction(time1)
fraction2 = to_day_fraction(time2)
Calendar.ISO.iso_days_to_unit({0, fraction1}, unit) -
Calendar.ISO.iso_days_to_unit({0, fraction2}, unit)
end
@doc """
Returns the given time with the microsecond field truncated to the given
precision (`:microsecond`, `millisecond` or `:second`).
The given time is returned unchanged if it already has lower precision than
the given precision.
## Examples
iex> Time.truncate(~T[01:01:01.123456], :microsecond)
~T[01:01:01.123456]
iex> Time.truncate(~T[01:01:01.123456], :millisecond)
~T[01:01:01.123]
iex> Time.truncate(~T[01:01:01.123456], :second)
~T[01:01:01]
"""
@doc since: "1.6.0"
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
def truncate(%Time{microsecond: microsecond} = time, precision) do
%{time | microsecond: Calendar.truncate(microsecond, precision)}
end
## Helpers
defp to_day_fraction(%{
hour: hour,
minute: minute,
second: second,
microsecond: {_, _} = microsecond,
calendar: calendar
}) do
calendar.time_to_day_fraction(hour, minute, second, microsecond)
end
defimpl String.Chars do
def to_string(time) do
%{
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: calendar
} = time
calendar.time_to_string(hour, minute, second, microsecond)
end
end
defimpl Inspect do
def inspect(time, _) do
%{
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: calendar
} = time
"~T[" <>
calendar.time_to_string(hour, minute, second, microsecond) <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
defp suffix(calendar), do: " " <> inspect(calendar)
end
end
@@ -1,96 +0,0 @@
defmodule Calendar.TimeZoneDatabase do
@moduledoc """
This module defines a behaviour for providing time zone data.
IANA provides time zone data that includes data about different
UTC offsets and standard offsets for time zones.
"""
@typedoc """
A period where a certain combination of UTC offset, standard offset and zone
abbreviation is in effect.
For instance one period could be the summer of 2018 in "Europe/London" where summer time /
daylight saving time is in effect and lasts from spring to autumn. At autumn the `std_offset`
changes along with the `zone_abbr` so a different period is needed during winter.
"""
@type time_zone_period :: %{
optional(any) => any,
utc_offset: Calendar.utc_offset(),
std_offset: Calendar.std_offset(),
zone_abbr: Calendar.zone_abbr()
}
@typedoc """
Limit for when a certain time zone period begins or ends.
A beginning is inclusive. An ending is exclusive. Eg. if a period is from
2015-03-29 01:00:00 and until 2015-10-25 01:00:00, the period includes and
begins from the beginning of 2015-03-29 01:00:00 and lasts until just before
2015-10-25 01:00:00.
A beginning or end for certain periods are infinite. For instance the latest
period for time zones without DST or plans to change. However for the purpose
of this behaviour they are only used for gaps in wall time where the needed
period limits are at a certain time.
"""
@type time_zone_period_limit :: Calendar.naive_datetime()
@doc """
Time zone period for a point in time in UTC for a specific time zone.
Takes a time zone name and a point in time for UTC and returns a
`time_zone_period` for that point in time.
"""
@doc since: "1.8.0"
@callback time_zone_period_from_utc_iso_days(Calendar.iso_days(), Calendar.time_zone()) ::
{:ok, time_zone_period}
| {:error, :time_zone_not_found | :utc_only_time_zone_database}
@doc """
Possible time zone periods for a certain time zone and wall clock date and time.
When the provided `datetime` is ambiguous a tuple with `:ambiguous` and two possible
periods. The periods in the list are sorted with the first element being the one that begins first.
When the provided `datetime` is in a gap - for instance during the "spring forward" when going
from winter time to summer time, a tuple with `:gap` and two periods with limits are returned
in a nested tuple. The first nested two-tuple is the period before the gap and a naive datetime
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`
and the `time_zone_period` is returned.
"""
@doc since: "1.8.0"
@callback time_zone_periods_from_wall_datetime(Calendar.naive_datetime(), Calendar.time_zone()) ::
{:ok, time_zone_period}
| {:ambiguous, time_zone_period, time_zone_period}
| {:gap, {time_zone_period, time_zone_period_limit},
{time_zone_period, time_zone_period_limit}}
| {:error, :time_zone_not_found | :utc_only_time_zone_database}
end
defmodule Calendar.UTCOnlyTimeZoneDatabase do
@moduledoc """
Built-in time zone database that works only in Etc/UTC.
For all other time zones, it returns `{:error, :utc_only_time_zone_database}`.
"""
@behaviour Calendar.TimeZoneDatabase
@impl true
def time_zone_period_from_utc_iso_days(_, "Etc/UTC"),
do: {:ok, %{std_offset: 0, utc_offset: 0, zone_abbr: "UTC"}}
def time_zone_period_from_utc_iso_days(_, _),
do: {:error, :utc_only_time_zone_database}
@impl true
def time_zone_periods_from_wall_datetime(_, "Etc/UTC"),
do: {:ok, %{std_offset: 0, utc_offset: 0, zone_abbr: "UTC"}}
def time_zone_periods_from_wall_datetime(_, _),
do: {:error, :utc_only_time_zone_database}
end
+543 -1712
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-152
View File
@@ -1,152 +0,0 @@
defmodule Code.Identifier do
@moduledoc false
@doc """
Checks if the given identifier is an unary op.
## Examples
iex> Code.Identifier.unary_op(:+)
{:non_associative, 300}
"""
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
def unary_op(op) do
cond do
op in [:&] -> {:non_associative, 90}
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
op in [:@] -> {:non_associative, 320}
true -> :error
end
end
@doc """
Checks if the given identifier is a binary op.
## Examples
iex> Code.Identifier.binary_op(:+)
{:left, 210}
"""
@spec binary_op(atom) :: {:left | :right, precedence :: pos_integer} | :error
def binary_op(op) do
cond do
op in [:<-, :\\] -> {:left, 40}
op in [:when] -> {:right, 50}
op in [:"::"] -> {:right, 60}
op in [:|] -> {:right, 70}
op in [:=] -> {:right, 100}
op in [:||, :|||, :or] -> {:left, 120}
op in [:&&, :&&&, :and] -> {:left, 130}
op in [:==, :!=, :=~, :===, :!==] -> {:left, 140}
op in [:<, :<=, :>=, :>] -> {:left, 150}
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
op in [:in] -> {:left, 170}
op in [:"^^^"] -> {:left, 180}
op in [:"//"] -> {:right, 190}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
op in [:**] -> {:left, 230}
op in [:.] -> {:left, 310}
true -> :error
end
end
@doc """
Extracts the name and arity of the parent from the anonymous function identifier.
"""
# Example of this format: -NAME/ARITY-fun-COUNT-
def extract_anonymous_fun_parent(atom) when is_atom(atom) do
with "-" <> rest <- Atom.to_string(atom),
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
else
_ -> :error
end
end
@doc """
Escapes the given identifier.
"""
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: iolist(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
(is_integer(limit) and limit >= 0) do
escape(binary, char, limit, [], fun)
end
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
{acc, binary}
end
defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), [acc | '\\\#{'], fun)
end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
escaped = if value = fun.(h), do: value, else: escape_char(h)
escape(t, char, decrement(count), [acc | escaped], fun)
end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), [acc | ['\\x', to_hex(a), to_hex(b)]], fun)
end
defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>}
end
defp escape_char(0), do: '\\0'
defp escape_char(65279), do: '\\uFEFF'
defp escape_char(char)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
<<char::utf8>>
end
defp escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
['\\x', to_hex(a), to_hex(b)]
end
defp escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
end
defp escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
end
defp escape_map(?\a), do: '\\a'
defp escape_map(?\b), do: '\\b'
defp escape_map(?\d), do: '\\d'
defp escape_map(?\e), do: '\\e'
defp escape_map(?\f), do: '\\f'
defp escape_map(?\n), do: '\\n'
defp escape_map(?\r), do: '\\r'
defp escape_map(?\t), do: '\\t'
defp escape_map(?\v), do: '\\v'
defp escape_map(?\\), do: '\\\\'
defp escape_map(_), do: false
@compile {:inline, to_hex: 1, decrement: 1}
defp to_hex(c) when c in 0..9, do: ?0 + c
defp to_hex(c) when c in 10..15, do: ?A + c - 10
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
end
-600
View File
@@ -1,600 +0,0 @@
defmodule Code.Normalizer do
@moduledoc false
defguard is_literal(x)
when is_integer(x) or
is_float(x) or
is_binary(x) or
is_atom(x)
@doc """
Wraps literals in the quoted expression to conform to the AST format expected
by the formatter.
"""
def normalize(quoted, opts \\ []) do
line = Keyword.get(opts, :line, nil)
escape = Keyword.get(opts, :escape, true)
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
state = %{
escape: escape,
parent_meta: [line: line],
locals_without_parens: locals_without_parens ++ Code.Formatter.locals_without_parens()
}
do_normalize(quoted, state)
end
# Wrapped literals should receive the block meta
defp do_normalize({:__block__, meta, [literal]}, state)
when not is_tuple(literal) or tuple_size(literal) == 2 do
normalize_literal(literal, meta, state)
end
# Only normalize the first argument of an alias if it's not an atom
defp do_normalize({:__aliases__, meta, [first | rest]}, state) when not is_atom(first) do
meta = patch_meta_line(meta, state.parent_meta)
first = do_normalize(first, %{state | parent_meta: meta})
{:__aliases__, meta, [first | rest]}
end
defp do_normalize({:__aliases__, _, _} = quoted, _state) do
quoted
end
# Skip captured arguments like &1
defp do_normalize({:&, meta, [term]}, state) when is_integer(term) do
meta = patch_meta_line(meta, state.parent_meta)
{:&, meta, [term]}
end
# Ranges
defp do_normalize(left..right//step, state) do
left = do_normalize(left, state)
right = do_normalize(right, state)
meta = meta_line(state)
if step == 1 do
{:.., meta, [left, right]}
else
step = do_normalize(step, state)
{:"..//", meta, [left, right, step]}
end
end
# Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state)
end
# Atoms with interpolations
defp do_normalize(
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta,
[{:<<>>, _, parts} = string, :utf8]},
state
)
when is_list(parts) do
dot_meta = patch_meta_line(dot_meta, state.parent_meta)
call_meta = patch_meta_line(call_meta, dot_meta)
utf8 =
if parts == [] or binary_interpolated?(parts) do
# a non-normalized :utf8 atom signals an atom interpolation
:utf8
else
normalize_literal(:utf8, [], state)
end
string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end
# Charlists with interpolations
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
Enum.map(parts, fn
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args} ->
args = normalize_args(args, state)
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args}
part when is_binary(part) ->
if state.escape do
maybe_escape_literal(part, state)
else
part
end
end)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
end
end
# Don't normalize the `Access` atom in access syntax
defp do_normalize({:., meta, [Access, :get]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
{:., meta, [Access, :get]}
end
# Only normalize the left side of the dot operator
# The right hand side is an atom in the AST but it's not an atom literal, so
# it should not be wrapped
defp do_normalize({:., meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
left = do_normalize(left, %{state | parent_meta: meta})
{:., meta, [left, right]}
end
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
normalize_args(quoted, state)
end
# left -> right
defp do_normalize({:->, meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
left = normalize_args(left, %{state | parent_meta: meta})
right = do_normalize(right, %{state | parent_meta: meta})
{:->, meta, [left, right]}
end
# Maps
defp do_normalize({:%{}, meta, args}, state) when is_list(args) do
meta =
if meta == [] do
line = state.parent_meta[:line]
[line: line, closing: [line: line]]
else
meta
end
state = %{state | parent_meta: meta}
args =
case args do
[{:|, pipe_meta, [left, right]}] ->
left = do_normalize(left, state)
right = normalize_map_args(right, state)
[{:|, pipe_meta, [left, right]}]
[{_, _, _} = call] ->
[do_normalize(call, state)]
args ->
normalize_map_args(args, state)
end
{:%{}, meta, args}
end
# Sigils
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
when is_list(args) and is_atom(sigil) do
case Atom.to_string(sigil) do
<<"sigil_", _name>> ->
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
_ ->
normalize_call(quoted, state)
end
end
# Tuples
defp do_normalize({:{}, meta, args} = quoted, state) when is_list(args) do
{last_arg, args} = List.pop_at(args, -1)
if args != [] and match?([_ | _], last_arg) and keyword?(last_arg) do
args = normalize_args(args, state)
kw_list = normalize_kw_args(last_arg, state, true)
{:{}, meta, args ++ kw_list}
else
normalize_call(quoted, state)
end
end
# Module attributes
defp do_normalize({:@, meta, [{name, name_meta, [value]}]}, state) do
value =
cond do
keyword?(value) and value != [] ->
normalize_kw_args(value, state, true)
is_list(value) ->
normalize_literal(value, meta, state)
true ->
do_normalize(value, state)
end
{:@, meta, [{name, name_meta, [value]}]}
end
# Regular blocks
defp do_normalize({:__block__, meta, args}, state) when is_list(args) do
{:__block__, meta, normalize_args(args, state)}
end
# Calls
defp do_normalize({_, _, args} = quoted, state) when is_list(args) do
normalize_call(quoted, state)
end
# Vars
defp do_normalize({_, _, context} = quoted, _state) when is_atom(context) do
quoted
end
# Literals
defp do_normalize(quoted, state) do
normalize_literal(quoted, [], state)
end
# Numbers
defp normalize_literal(number, meta, state) when is_number(number) do
meta =
meta
|> Keyword.put_new(:token, inspect(number))
|> patch_meta_line(state.parent_meta)
{:__block__, meta, [number]}
end
# Atom, Strings
defp normalize_literal(literal, meta, state) when is_atom(literal) or is_binary(literal) do
meta = patch_meta_line(meta, state.parent_meta)
literal = maybe_escape_literal(literal, state)
if is_atom(literal) and Macro.classify_atom(literal) == :alias and
is_nil(meta[:delimiter]) do
segments =
case Atom.to_string(literal) do
"Elixir" ->
[:"Elixir"]
"Elixir." <> segments ->
segments
|> String.split(".")
|> Enum.map(&String.to_atom/1)
end
{:__aliases__, meta, segments}
else
{:__block__, meta, [literal]}
end
end
# 2-tuples
defp normalize_literal({left, right}, meta, state) do
meta = patch_meta_line(meta, state.parent_meta)
state = %{state | parent_meta: meta}
if match?([_ | _], right) and keyword?(right) do
{:__block__, meta, [{do_normalize(left, state), normalize_kw_args(right, state, true)}]}
else
{:__block__, meta, [{do_normalize(left, state), do_normalize(right, state)}]}
end
end
# Lists
defp normalize_literal(list, meta, state) when is_list(list) do
if list != [] and List.ascii_printable?(list) do
# It's a charlist
list =
if state.escape do
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(string) |> to_charlist()
else
list
end
meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
{:__block__, meta, [list]}
else
meta =
if line = state.parent_meta[:line] do
meta
|> Keyword.put_new(:closing, line: line)
|> patch_meta_line(state.parent_meta)
else
meta
end
{:__block__, meta, [normalize_kw_args(list, state, false)]}
end
end
# Probably an invalid value, wrap it and send it upstream
defp normalize_literal(quoted, meta, _state) do
{:__block__, meta, [quoted]}
end
defp normalize_call({form, meta, args}, state) do
meta = patch_meta_line(meta, state.parent_meta)
arity = length(args)
# Only normalize the form if it's a qualified call
form =
if is_atom(form) do
form
else
do_normalize(form, %{state | parent_meta: meta})
end
meta =
if is_nil(meta[:no_parens]) and is_nil(meta[:closing]) and is_nil(meta[:do]) and
not Code.Formatter.local_without_parens?(form, arity, state.locals_without_parens) do
[closing: [line: meta[:line]]] ++ meta
else
meta
end
cond do
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], List.last(args)) ->
# Non normalized kw blocks
line = state.parent_meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
allow_keyword?(form, arity) ->
args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, [])
last_args =
case last_arg do
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
if key_meta[:format] == :keyword do
last_args
else
[last_arg]
end
[] ->
[]
_ ->
[last_arg]
end
{form, meta, leading_args ++ last_args}
true ->
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
end
end
defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
meta = patch_meta_line(meta, state.parent_meta)
parts =
if binary_interpolated?(parts) do
normalize_interpolation_parts(parts, %{state | parent_meta: meta}, escape_interpolation)
else
state = %{state | parent_meta: meta}
Enum.map(parts, fn part ->
with {:"::", meta, [left, _]} <- part,
true <- meta[:inferred_bitstring_spec] do
do_normalize(left, state)
else
_ -> do_normalize(part, state)
end
end)
end
{:<<>>, meta, parts}
end
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
Enum.map(parts, fn
{:"::", interpolation_meta,
[
{{:., dot_meta, [Kernel, :to_string]}, middle_meta, [middle]},
{:binary, binary_meta, context}
]} ->
middle = do_normalize(middle, %{state | parent_meta: dot_meta})
{:"::", interpolation_meta,
[
{{:., dot_meta, [Kernel, :to_string]}, middle_meta, [middle]},
{:binary, binary_meta, context}
]}
part ->
if escape_interpolation do
maybe_escape_literal(part, state)
else
part
end
end)
end
defp normalize_map_args(args, state) do
Enum.map(normalize_kw_args(args, state, false), fn
{:__block__, _, [{_, _} = pair]} -> pair
pair -> pair
end)
end
defp normalize_kw_blocks(form, meta, args, state) do
{kw_blocks, leading_args} = List.pop_at(args, -1)
kw_blocks =
Enum.map(kw_blocks, fn {tag, block} ->
block = do_normalize(block, %{state | parent_meta: meta})
block =
case block do
{_, _, [[{:->, _, _} | _] = block]} -> block
block -> block
end
# Only wrap the tag if it isn't already wrapped
tag =
case tag do
{:__block__, _, _} -> tag
_ -> {:__block__, [line: meta[:line]], [tag]}
end
{tag, block}
end)
leading_args = normalize_args(leading_args, %{state | parent_meta: meta})
{form, meta, leading_args ++ [kw_blocks]}
end
defp normalize_kw_args(elems, state, keyword?)
defp normalize_kw_args(
[{{:__block__, key_meta, [key]}, value} = first | rest] = current,
state,
keyword?
)
when is_atom(key) do
keyword? = keyword? or keyword?(current)
first =
if key_meta[:format] == :keyword and not keyword? do
key_meta = Keyword.delete(key_meta, :format)
line = key_meta[:line] || meta_line(state)
{:__block__, [line: line], [{{:__block__, key_meta, [key]}, value}]}
else
first
end
[first | normalize_kw_args(rest, state, keyword?)]
end
defp normalize_kw_args([{left, right} | rest] = current, state, keyword?) do
keyword? = keyword? or keyword?(current)
left =
if keyword? do
meta = [format: :keyword] ++ meta_line(state)
{:__block__, meta, [maybe_escape_literal(left, state)]}
else
do_normalize(left, state)
end
right = do_normalize(right, state)
pair =
with {:__block__, meta, _} <- left,
:keyword <- meta[:format] do
{left, right}
else
_ -> {:__block__, meta_line(state), [{left, right}]}
end
[pair | normalize_kw_args(rest, state, keyword?)]
end
defp normalize_kw_args([first | rest], state, keyword?) do
[do_normalize(first, state) | normalize_kw_args(rest, state, keyword?)]
end
defp normalize_kw_args([], _state, _keyword?) do
[]
end
defp normalize_args(args, state) do
Enum.map(args, &do_normalize(&1, state))
end
defp maybe_escape_literal(string, %{escape: true}) when is_binary(string) do
{string, _} = Code.Identifier.escape(string, nil)
IO.iodata_to_binary(string)
end
defp maybe_escape_literal(atom, %{escape: true} = state) when is_atom(atom) do
atom
|> Atom.to_string()
|> maybe_escape_literal(state)
|> String.to_atom()
end
defp maybe_escape_literal(term, _) do
term
end
defp binary_interpolated?(parts) do
Enum.all?(parts, fn
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true
binary when is_binary(binary) -> true
_ -> false
end)
end
defp list_interpolated?(parts) do
Enum.all?(parts, fn
{{:., _, [Kernel, :to_string]}, _, [_]} -> true
binary when is_binary(binary) -> true
_ -> false
end)
end
defp patch_meta_line(meta, parent_meta) do
with nil <- meta[:line],
line when is_integer(line) <- parent_meta[:line] do
[line: line] ++ meta
else
_ -> meta
end
end
defp meta_line(state) do
if line = state.parent_meta[:line] do
[line: line]
else
[]
end
end
defp keyword?([{{:__block__, key_meta, [key]}, _} | rest]) when is_atom(key) do
if key_meta[:format] == :keyword do
keyword?(rest)
else
false
end
end
defp keyword?([{key, _value} | rest]) when is_atom(key) do
case Atom.to_charlist(key) do
'Elixir.' ++ _ -> false
_ -> keyword?(rest)
end
end
defp keyword?([]), do: true
defp keyword?(_other), do: false
end
-423
View File
@@ -1,423 +0,0 @@
defmodule Code.Typespec do
@moduledoc false
@doc """
Converts a spec clause back to Elixir quoted expression.
"""
@spec spec_to_quoted(atom, tuple) :: {atom, keyword, [Macro.t()]}
def spec_to_quoted(name, spec)
def spec_to_quoted(name, {:type, anno, :fun, [{:type, _, :product, args}, result]})
when is_atom(name) do
meta = meta(anno)
body = {name, meta, Enum.map(args, &typespec_to_quoted/1)}
vars =
for type_expr <- args ++ [result],
var <- collect_vars(type_expr),
uniq: true,
do: {var, {:var, meta, nil}}
spec = {:"::", meta, [body, typespec_to_quoted(result)]}
if vars == [] do
spec
else
{:when, meta, [spec, vars]}
end
end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
guards =
for {:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, type]]} <- constrs do
{erl_to_ex_var(var), typespec_to_quoted(type)}
end
ignore_vars = Keyword.keys(guards)
vars =
for type_expr <- args ++ [result],
var <- collect_vars(type_expr),
var not in ignore_vars,
uniq: true,
do: {var, {:var, meta, nil}}
args = for arg <- args, do: typespec_to_quoted(arg)
when_args = [
{:"::", meta, [{name, meta, args}, typespec_to_quoted(result)]},
guards ++ vars
]
{:when, meta, when_args}
end
@doc """
Converts a type clause back to Elixir AST.
"""
def type_to_quoted(type)
def type_to_quoted({{:record, record}, fields, args}) when is_atom(record) do
fields = for field <- fields, do: typespec_to_quoted(field)
args = for arg <- args, do: typespec_to_quoted(arg)
type = {:{}, [], [record | fields]}
quote(do: unquote(record)(unquote_splicing(args)) :: unquote(type))
end
def type_to_quoted({name, type, args}) when is_atom(name) do
args = for arg <- args, do: typespec_to_quoted(arg)
quote(do: unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_quoted(type)))
end
@doc """
Returns all types available from the module's BEAM code.
The result is returned as a list of tuples where the first
element is the type (`:typep`, `:type` and `:opaque`).
The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang
Abstract Format.
"""
@spec fetch_types(module | binary) :: {:ok, [tuple]} | :error
def fetch_types(module) when is_atom(module) or is_binary(module) do
case typespecs_abstract_code(module) do
{:ok, abstract_code} ->
exported_types = for {:attribute, _, :export_type, types} <- abstract_code, do: types
exported_types = List.flatten(exported_types)
types =
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
kind in [:opaque, :type] do
cond do
kind == :opaque -> {:opaque, type}
{name, length(args)} in exported_types -> {:type, type}
true -> {:typep, type}
end
end
{:ok, types}
_ ->
:error
end
end
@doc """
Returns all specs available from the module's BEAM code.
The result is returned as a list of tuples where the first
element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang
Abstract Format.
"""
@spec fetch_specs(module) :: {:ok, [tuple]} | :error
def fetch_specs(module) when is_atom(module) or is_binary(module) do
case typespecs_abstract_code(module) do
{:ok, abstract_code} ->
{:ok, for({:attribute, _, :spec, value} <- abstract_code, do: value)}
:error ->
:error
end
end
@doc """
Returns all callbacks available from the module's BEAM code.
The result is returned as a list of tuples where the first
element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file
which can be located by the runtime system. The types will be
in the Erlang Abstract Format.
"""
@spec fetch_callbacks(module) :: {:ok, [tuple]} | :error
def fetch_callbacks(module) when is_atom(module) or is_binary(module) do
case typespecs_abstract_code(module) do
{:ok, abstract_code} ->
{:ok, for({:attribute, _, :callback, value} <- abstract_code, do: value)}
:error ->
:error
end
end
defp typespecs_abstract_code(module) do
with {module, binary} <- get_module_and_beam(module),
{:ok, {_, [debug_info: {:debug_info_v1, backend, data}]}} <-
:beam_lib.chunks(binary, [:debug_info]) do
case data do
{:elixir_v1, %{}, specs} ->
# Fast path to avoid translation to Erlang from Elixir.
{:ok, specs}
_ ->
case backend.debug_info(:erlang_v1, module, data, []) do
{:ok, abstract_code} -> {:ok, abstract_code}
_ -> :error
end
end
else
_ -> :error
end
end
defp get_module_and_beam(module) when is_atom(module) do
with {^module, beam, _filename} <- :code.get_object_code(module),
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
{module, beam}
else
_ -> :error
end
end
defp get_module_and_beam(beam) when is_binary(beam) do
case :beam_lib.info(beam) do
[_ | _] = info -> {info[:module], beam}
_ -> :error
end
end
## To AST conversion
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
[]
end
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
Enum.flat_map(args, &collect_vars/1)
end
defp collect_vars({:remote_type, _anno, args}) when is_list(args) do
Enum.flat_map(args, &collect_vars/1)
end
defp collect_vars({:typed_record_field, _anno, type}) do
collect_vars(type)
end
defp collect_vars({:paren_type, _anno, [type]}) do
collect_vars(type)
end
defp collect_vars({:var, _anno, var}) do
[erl_to_ex_var(var)]
end
defp collect_vars(_) do
[]
end
defp typespec_to_quoted({:user_type, anno, name, args}) do
args = for arg <- args, do: typespec_to_quoted(arg)
{name, meta(anno), args}
end
defp typespec_to_quoted({:type, anno, :tuple, :any}) do
{:tuple, meta(anno), []}
end
defp typespec_to_quoted({:type, anno, :tuple, args}) do
args = for arg <- args, do: typespec_to_quoted(arg)
{:{}, meta(anno), args}
end
defp typespec_to_quoted({:type, _anno, :list, [{:type, _, :union, unions} = arg]}) do
case unpack_typespec_kw(unions, []) do
{:ok, ast} -> ast
:error -> [typespec_to_quoted(arg)]
end
end
defp typespec_to_quoted({:type, anno, :list, []}) do
{:list, meta(anno), []}
end
defp typespec_to_quoted({:type, _anno, :list, [arg]}) do
[typespec_to_quoted(arg)]
end
defp typespec_to_quoted({:type, anno, :nonempty_list, []}) do
[{:..., meta(anno), nil}]
end
defp typespec_to_quoted({:type, anno, :nonempty_list, [arg]}) do
[typespec_to_quoted(arg), {:..., meta(anno), nil}]
end
defp typespec_to_quoted({:type, anno, :map, :any}) do
{:map, meta(anno), []}
end
defp typespec_to_quoted({:type, anno, :map, fields}) do
fields =
Enum.map(fields, fn
{:type, _, :map_field_assoc, :any} ->
{{:optional, [], [{:any, [], []}]}, {:any, [], []}}
{:type, _, :map_field_exact, [{:atom, _, k}, v]} ->
{k, typespec_to_quoted(v)}
{:type, _, :map_field_exact, [k, v]} ->
{{:required, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
{:type, _, :map_field_assoc, [k, v]} ->
{{: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 = {:%{}, meta(anno), fields_pruned}
{:%, meta(anno), [struct, map_pruned]}
_ ->
{:%{}, meta(anno), fields}
end
end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
{arg1, 0} ->
quote(line: line, do: <<_::unquote(arg1)>>)
{0, arg2} ->
quote(line: line, do: <<_::_*unquote(arg2)>>)
{arg1, arg2} ->
quote(line: line, do: <<_::unquote(arg1), _::_*unquote(arg2)>>)
end
end
defp typespec_to_quoted({:type, anno, :union, args}) do
args = for arg <- args, do: typespec_to_quoted(arg)
Enum.reduce(Enum.reverse(args), fn arg, expr -> {:|, meta(anno), [arg, expr]} end)
end
defp typespec_to_quoted({:type, anno, :fun, [{:type, _, :product, args}, result]}) do
args = for arg <- args, do: typespec_to_quoted(arg)
[{:->, meta(anno), [args, typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, [args, result]}) do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end
defp typespec_to_quoted({:type, _anno, nil, []}) do
[]
end
defp typespec_to_quoted({:type, anno, name, args}) do
args = for arg <- args, do: typespec_to_quoted(arg)
{name, meta(anno), args}
end
defp typespec_to_quoted({:var, anno, var}) do
{erl_to_ex_var(var), meta(anno), nil}
end
defp typespec_to_quoted({:op, anno, op, arg}) do
{op, meta(anno), [typespec_to_quoted(arg)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args)
end
defp typespec_to_quoted({:ann_type, anno, [var, type]}) do
{:"::", meta(anno), [typespec_to_quoted(var), typespec_to_quoted(type)]}
end
defp typespec_to_quoted(
{:typed_record_field, {:record_field, anno1, {:atom, anno2, name}}, type}
) do
typespec_to_quoted({:ann_type, anno1, [{:var, anno2, name}, type]})
end
defp typespec_to_quoted({:type, _, :any}) do
quote(do: ...)
end
defp typespec_to_quoted({:paren_type, _, [type]}) do
typespec_to_quoted(type)
end
defp typespec_to_quoted({type, _anno, atom}) when is_atom(type) do
atom
end
defp typespec_to_quoted(other), do: other
## Helpers
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :charlist}, []) do
typespec_to_quoted({:type, anno, :charlist, []})
end
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :nonempty_charlist}, []) do
typespec_to_quoted({:type, anno, :nonempty_charlist, []})
end
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :struct}, []) do
typespec_to_quoted({:type, anno, :struct, []})
end
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :as_boolean}, [arg]) do
typespec_to_quoted({:type, anno, :as_boolean, [arg]})
end
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :keyword}, args) do
typespec_to_quoted({:type, anno, :keyword, args})
end
defp remote_type(anno, mod, name, args) do
args = for arg <- args, do: typespec_to_quoted(arg)
dot = {:., meta(anno), [typespec_to_quoted(mod), typespec_to_quoted(name)]}
{dot, meta(anno), args}
end
defp erl_to_ex_var(var) do
case Atom.to_string(var) do
<<"_", c::utf8, rest::binary>> ->
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
<<c::utf8, rest::binary>> ->
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
end
end
defp unpack_typespec_kw([{:type, _, :tuple, [{:atom, _, atom}, type]} | t], acc) do
unpack_typespec_kw(t, [{atom, typespec_to_quoted(type)} | acc])
end
defp unpack_typespec_kw([], acc) do
{:ok, Enum.reverse(acc)}
end
defp unpack_typespec_kw(_, _acc) do
:error
end
defp meta(anno), do: [line: :erl_anno.line(anno)]
end
+33 -135
View File
@@ -17,163 +17,61 @@ defprotocol Collectable do
This design is intentional. `Enumerable` was designed to support infinite
collections, resources and other structures with fixed shape. For example,
it doesn't make sense to insert values into a `Range`, as it has a
fixed shape where only the range limits and step are stored.
it doesn't make sense to insert values into a range, as it has a fixed
shape where just the range limits are stored.
The `Collectable` module was designed to fill the gap left by the
`Enumerable` protocol. `Collectable.into/1` can be seen as the opposite of
`Enumerable.reduce/3`. If the functions in `Enumerable` are about taking values out,
then `Collectable.into/1` is about collecting those values into a structure.
## Examples
To show how to manually use the `Collectable` protocol, let's play with a
simplified implementation for `MapSet`.
iex> {initial_acc, collector_fun} = Collectable.into(MapSet.new())
iex> updated_acc = Enum.reduce([1, 2, 3], initial_acc, fn elem, acc ->
...> collector_fun.(acc, {:cont, elem})
...> end)
iex> collector_fun.(updated_acc, :done)
MapSet.new([1, 2, 3])
To show how the protocol can be implemented, we can again look at the
simplified implementation for `MapSet`. In this implementation "collecting" elements
simply means inserting them in the set through `MapSet.put/2`.
defimpl Collectable, for: MapSet do
def into(map_set) do
collector_fun = fn
map_set_acc, {:cont, elem} ->
MapSet.put(map_set_acc, elem)
map_set_acc, :done ->
map_set_acc
_map_set_acc, :halt ->
:ok
end
initial_acc = map_set
{initial_acc, collector_fun}
end
end
So now we can call `Enum.into/2`:
iex> Enum.into([1, 2, 3], MapSet.new())
MapSet.new([1, 2, 3])
`Enumerable` protocol. `into/1` can be seen as the opposite of
`Enumerable.reduce/3`. If `Enumerable` is about taking values out,
`Collectable.into/1` is about collecting those values into a structure.
"""
@type command :: {:cont, term} | :done | :halt
@doc """
Returns an initial accumulator and a "collector" function.
Returns a function that collects values alongside
the initial accumulation value.
Receives a `collectable` which can be used as the initial accumulator that will
be passed to the function.
The returned function receives a collectable and injects a given
value into it for every `{:cont, term}` instruction.
The collector function receives a term and a command and injects the term into
the collectable accumulator on every `{:cont, term}` command.
`:done` is passed when no further values will be injected, useful
for closing resources and normalizing values. A collectable must
be returned on `:done`.
`:done` is passed as a command when no further values will be injected. This
is useful when there's a need to close resources or normalizing values. A
collectable must be returned when the command is `:done`.
If injection is suddenly interrupted, `:halt` is passed and the function
can return any value as it won't be used.
For examples on how to use the `Collectable` protocol and `into/1` see the
module documentation.
If injection is suddenly interrupted, `:halt` is passed and it can
return any value, as it won't be used.
"""
@spec into(t) :: {initial_acc :: term, collector :: (term, command -> t | term)}
@spec into(t) :: {term, (term, command -> t | term)}
def into(collectable)
end
defimpl Collectable, for: List do
def into(list) do
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
if list != [] do
IO.warn(
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
"list, consider concatenating the two lists with the ++ operator."
)
end
fun = fn
list_acc, {:cont, elem} ->
[elem | list_acc]
list_acc, :done ->
list ++ :lists.reverse(list_acc)
_list_acc, :halt ->
:ok
end
{[], fun}
def into(original) do
{[], fn
list, {:cont, x} -> [x | list]
list, :done -> original ++ :lists.reverse(list)
_, :halt -> :ok
end}
end
end
defimpl Collectable, for: BitString do
def into(binary) when is_binary(binary) do
fun = fn
acc, {:cont, x} when is_binary(x) and is_list(acc) ->
[acc | x]
acc, {:cont, x} when is_bitstring(x) and is_bitstring(acc) ->
<<acc::bitstring, x::bitstring>>
acc, {:cont, x} when is_bitstring(x) ->
<<IO.iodata_to_binary(acc)::bitstring, x::bitstring>>
acc, :done when is_bitstring(acc) ->
acc
acc, :done ->
IO.iodata_to_binary(acc)
__acc, :halt ->
:ok
end
{[binary], fun}
end
def into(bitstring) do
fun = fn
acc, {:cont, x} when is_bitstring(x) ->
<<acc::bitstring, x::bitstring>>
acc, :done ->
acc
_acc, :halt ->
:ok
end
{bitstring, fun}
def into(original) do
{original, fn
acc, {:cont, x} when is_bitstring(x) -> [acc | x]
acc, :done -> IO.iodata_to_binary(acc)
_, :halt -> :ok
end}
end
end
defimpl Collectable, for: Map do
def into(map) do
fun = fn
map_acc, {:cont, {key, value}} ->
Map.put(map_acc, key, value)
map_acc, :done ->
map_acc
_map_acc, :halt ->
:ok
end
{map, fun}
def into(original) do
{original, fn
map, {:cont, {k, v}} -> :maps.put(k, v, map)
map, :done -> map
_, :halt -> :ok
end}
end
end
-359
View File
@@ -1,359 +0,0 @@
defmodule Config do
@moduledoc ~S"""
A simple keyword-based configuration API.
## Example
This module is most commonly used to define application configuration,
typically in `config/config.exs`:
import Config
config :some_app,
key1: "value1",
key2: "value2"
import_config "#{config_env()}.exs"
`import Config` will import the functions `config/2`, `config/3`
`config_env/0`, `config_target/0`, and `import_config/1`
to help you manage your configuration.
`config/2` and `config/3` are used to define key-value configuration
for a given application. Once Mix starts, it will automatically
evaluate the configuration file and persist the configuration above
into `:some_app`'s application environment, which can be accessed in
as follows:
"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`.
`Config` also provides a low-level API for evaluating and reading
configuration, under the `Config.Reader` module.
> **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. Also note that
> the `config/config.exs` of a library is not evaluated when the library is
> used as a dependency, as configuration is always meant to configure the
> current project. For more information, read our [library guidelines](library-guidelines.md).
## 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
step is to replace `use Mix.Config` at the top of your config files by
`import Config`.
The second is to make sure your `import_config/1` calls do not have a
wildcard character. If so, you need to perform the wildcard lookup
manually. For example, if you did:
import_config "../apps/*/config/config.exs"
It has to be replaced by:
for config <- "../apps/*/config/config.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
import_config config
end
The last step is to replace all `Mix.env()` calls in the config files with `config_env()`.
Keep in mind you must also avoid using `Mix.env()` inside your project files.
To check the environment at _runtime_, you may add a configuration key:
# config.exs
...
config :my_app, env: config_env()
Then, in other scripts and modules, you may get the environment with
`Application.fetch_env!/2`:
# router.exs
...
if Application.fetch_env!(:my_app, :env) == :prod do
...
end
The only files where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which always within the
`Mix.Tasks` namespace.
## 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`).
"""
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports}
defp get_opts!(), do: Process.get(@opts_key) || raise_improper_use!()
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!()
defp put_config(value), do: Process.put(@config_key, value)
defp delete_config(), do: Process.delete(@config_key)
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 raise_improper_use!() do
raise "could not set configuration via Config. " <>
"This usually means you are trying to execute a configuration file " <>
"directly, instead of reading it with Config.Reader"
end
@doc """
Configures the given `root_key`.
Keyword lists are always deep-merged.
## Examples
The given `opts` are merged into the existing configuration
for the given `root_key`. Conflicting keys are overridden by the
ones specified in `opts`, unless they are keywords, which are
deep merged recursively. For example, the application configuration
below
config :logger,
level: :warn,
backends: [:console]
config :logger,
level: :info,
truncate: 1024
will have a final configuration for `:logger` of:
[level: :info, backends: [:console], truncate: 1024]
"""
@doc since: "1.9.0"
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end
get_config!()
|> __merge__([{root_key, opts}])
|> put_config()
end
@doc """
Configures the given `key` for the given `root_key`.
Keyword lists are always deep merged.
## Examples
The given `opts` are merged into the existing values for `key`
in the given `root_key`. Conflicting keys are overridden by the
ones specified in `opts`, unless they are keywords, which are
deep merged recursively. For example, the application configuration
below
config :ecto, Repo,
log_level: :warn,
adapter: Ecto.Adapters.Postgres,
metadata: [read_only: true]
config :ecto, Repo,
log_level: :info,
pool_size: 10,
metadata: [replica: true]
will have a final value of the configuration for the `Repo`
key in the `:ecto` application of:
Application.get_env(:ecto, Repo)
#=> [
#=> log_level: :info,
#=> pool_size: 10,
#=> adapter: Ecto.Adapters.Postgres,
#=> metadata: [read_only: true, replica: true]
#=> ]
"""
@doc since: "1.9.0"
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
get_config!()
|> __merge__([{root_key, [{key, opts}]}])
|> 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.
In case the file doesn't exist, an error is raised.
If file is a relative, it will be expanded relatively to the
directory the current configuration file is in.
## Examples
This is often used to emulate configuration across environments:
import_config "#{config_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
quote do
Config.__import__!(Path.expand(unquote(file), __DIR__))
:ok
end
end
@doc false
@spec __import__!(Path.t()) :: {term, Code.binding()}
def __import__!(file) when is_binary(file) do
import_config!(file, File.read!(file), true)
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})
previous_config = put_config([])
previous_imports = put_imports(imports)
try do
{eval_config, _} = import_config!(file, content, false)
case get_config!() do
[] when is_list(eval_config) ->
{validate!(eval_config, file), get_imports!()}
pdict_config ->
{pdict_config, get_imports!()}
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()
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
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 ->
Keyword.merge(app1, app2, &deep_merge/3)
end)
end
defp deep_merge(_key, value1, value2) do
if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
Keyword.merge(value1, value2, &deep_merge/3)
else
value2
end
end
defp validate!(config, file) do
Enum.all?(config, fn
{app, value} when is_atom(app) ->
if Keyword.keyword?(value) do
true
else
raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}"
end
_ ->
false
end)
config
end
end
-415
View File
@@ -1,415 +0,0 @@
defmodule Config.Provider do
@moduledoc """
Specifies a provider API that loads configuration during boot.
Config providers are typically used during releases to load
external configuration while the system boots. This is done
by starting the VM with the minimum amount of applications
running, then invoking all of the providers, and then
restarting the system. This requires a mutable configuration
file on disk, as the results of the providers are written to
the file system. For more information on runtime configuration,
see `mix release`.
## Multiple config files
One common use of config providers is to specify multiple
configuration files in a release. Elixir ships with one provider,
called `Config.Reader`, which is capable of handling Elixir's
built-in config files.
For example, imagine you want to list some basic configuration
on Mix's built-in `config/runtime.exs` file, but you also want
to support additional configuration files. To do so, you can add
this inside the `def project` portion of your `mix.exs`:
releases: [
demo: [
config_providers: [
{Config.Reader, {:system, "RELEASE_ROOT", "/extra_config.exs"}}
]
]
]
You can place this `extra_config.exs` file in your release in
multiple ways:
1. If it is available on the host when assembling the release,
you can place it on "rel/overlays/extra_config.exs" and it
will be automatically copied to the release root
2. If it is available on the target during deployment, you can
simply copy it to the release root as a step in your deployment
Now once the system boots, it will load both `config/runtime.exs`
and `extra_config.exs` early in the boot process. You can learn
more options on `Config.Reader`.
## Custom config provider
You can also implement custom config providers, similar to how
`Config.Reader` works. For example, imagine you need to load
some configuration from a JSON file and load that into the system.
Said configuration provider would look like:
defmodule JSONConfigProvider do
@behaviour Config.Provider
# Let's pass the path to the JSON file as config
@impl true
def init(path) when is_binary(path), do: path
@impl true
def load(config, path) do
# We need to start any app we may depend on.
{:ok, _} = Application.ensure_all_started(:jason)
json = path |> File.read!() |> Jason.decode!()
Config.Reader.merge(
config,
my_app: [
some_value: json["my_app_some_value"],
another_value: json["my_app_another_value"],
]
)
end
end
Then, when specifying your release, you can specify the provider in
the release configuration:
releases: [
demo: [
config_providers: [
{JSONConfigProvider, "/etc/config.json"}
]
]
]
"""
@type config :: keyword
@type state :: term
@typedoc """
A path pointing to a configuration file.
Since configuration files are often accessed on target machines,
it can be expressed either as:
* a binary representing an absolute path
* a `{:system, system_var, path}` tuple where the config is the
concatenation of the environment variable `system_var` with
the given `path`
"""
@type config_path :: {:system, binary(), binary()} | binary()
@doc """
Invoked when initializing a config provider.
A config provider is typically initialized on the machine
where the system is assembled and not on the target machine.
The `c:init/1` callback is useful to verify the arguments
given to the provider and prepare the state that will be
given to `c:load/2`.
Furthermore, because the state returned by `c:init/1` can
be written to text-based config files, it should be
restricted only to simple data types, such as integers,
strings, atoms, tuples, maps, and lists. Entries such as
PIDs, references, and functions cannot be serialized.
"""
@callback init(term) :: state
@doc """
Loads configuration (typically during system boot).
It receives the current `config` and the `state` returned by
`c:init/1`. Then, you typically read the extra configuration
from an external source and merge it into the received `config`.
Merging should be done with `Config.Reader.merge/2`, as it
performs deep merge. It should return the updated config.
Note that `c:load/2` is typically invoked very early in the
boot process, therefore if you need to use an application
in the provider, it is your responsibility to start it.
"""
@callback load(config, state) :: config
@doc false
defstruct [
:providers,
:config_path,
extra_config: [],
prune_runtime_sys_config_after_boot: false,
reboot_system_after_config: false,
validate_compile_env: false
]
@reserved_apps [:kernel, :stdlib]
@doc """
Validates a `t:config_path/0`.
"""
@doc since: "1.9.0"
@spec validate_config_path!(config_path) :: :ok
def validate_config_path!({:system, name, path})
when is_binary(name) and is_binary(path),
do: :ok
def validate_config_path!(path) do
if is_binary(path) and Path.type(path) != :relative do
:ok
else
raise ArgumentError, """
expected configuration path to be:
* a binary representing an absolute path
* a tuple {:system, system_var, path} where the config is the \
concatenation of the `system_var` with the given `path`
Got: #{inspect(path)}
"""
end
end
@doc """
Resolves a `t:config_path/0` to an actual path.
"""
@doc since: "1.9.0"
@spec resolve_config_path!(config_path) :: binary
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}]]
end
@doc false
def boot(reboot_fun \\ &restart_and_sleep/0) do
# 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
{:ok, {:booted, path}} ->
path && File.rm(path)
with {:ok, %Config.Provider{} = provider} <- Application.fetch_env(:elixir, @init_key) do
maybe_validate_compile_env(provider)
end
:booted
_ ->
case Application.fetch_env(:elixir, @init_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)
_ ->
:skip
end
end
end
defp boot_providers(path, provider, reboot_config, reboot_fun) do
original_config = read_config!(path)
config =
original_config
|> Config.__merge__(provider.extra_config)
|> run_providers(provider)
if provider.reboot_system_after_config do
config
|> Config.__merge__(reboot_config)
|> write_config!(path)
reboot_fun.()
else
for app <- @reserved_apps, config[app] != original_config[app] do
abort("""
Cannot configure #{inspect(app)} because :reboot_system_after_config has been set \
to false and #{inspect(app)} has already been loaded, meaning any further \
configuration won't have an effect.
The configuration for #{inspect(app)} before config providers was:
#{inspect(original_config[app])}
The configuration for #{inspect(app)} after config providers was:
#{inspect(config[app])}
""")
end
_ = Application.put_all_env(config, persistent: true)
maybe_validate_compile_env(provider)
:ok
end
end
defp maybe_validate_compile_env(provider) do
with [_ | _] = compile_env <- provider.validate_compile_env do
validate_compile_env(compile_env)
end
end
@doc false
def validate_compile_env(compile_env, ensure_loaded? \\ true) do
for {app, [key | path], compile_return} <- compile_env,
ensure_app_loaded?(app, ensure_loaded?) do
try do
traverse_env(Application.fetch_env(app, key), path)
rescue
e ->
abort("""
application #{inspect(app)} failed reading its compile environment #{path(key, path)}:
#{Exception.format(:error, e, __STACKTRACE__)}
Expected it to match the compile time value of #{return_to_text(compile_return)}.
#{compile_env_tips(app)}
""")
else
^compile_return ->
:ok
runtime_return ->
abort("""
the application #{inspect(app)} has a different value set #{path(key, path)} \
during runtime compared to compile time. Since this application environment entry was \
marked as compile time, this difference can lead to different behaviour than expected:
* Compile time value #{return_to_text(compile_return)}
* Runtime value #{return_to_text(runtime_return)}
#{compile_env_tips(app)}
""")
end
end
: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)}"
defp compile_env_tips(app),
do: """
To fix this error, you might:
* Make the runtime value match the compile time one
* Recompile your project. If the misconfigured application is a dependency, \
you may need to run "mix deps.compile #{app} --force"
* Alternatively, you can disable this check. If you are using releases, you can \
set :validate_compile_env to false in your release configuration. If you are \
using Mix to start your system, you can pass the --no-validate-compile-env flag
"""
defp return_to_text({:ok, value}), do: "was set to: #{inspect(value)}"
defp return_to_text(:error), do: "was not set"
defp traverse_env(return, []), do: return
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)
if mode in [:embedded, :interactive] do
:init.restart(mode: mode)
else
:init.restart()
end
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 read_config!(path) do
case :file.consult(path) do
{:ok, [inner]} ->
inner
{:error, reason} ->
bad_path_abort(
"Could not read runtime configuration due to reason: #{inspect(reason)}",
path
)
end
end
defp run_providers(config, %{providers: providers}) do
Enum.reduce(providers, config, fn {provider, state}, acc ->
try do
provider.load(acc, state)
catch
kind, error ->
IO.puts(:stderr, "ERROR! Config provider #{inspect(provider)} failed with:")
IO.puts(:stderr, Exception.format(kind, error, __STACKTRACE__))
:erlang.raise(kind, error, __STACKTRACE__)
else
term when is_list(term) ->
term
term ->
abort("Expected provider #{inspect(provider)} to return a list, got: #{inspect(term)}")
end
end)
end
defp write_config!(config, path) do
contents = :io_lib.format("%% coding: utf-8~n~tw.~n", [config])
case File.write(path, IO.chardata_to_string(contents)) do
:ok ->
:ok
{:error, reason} ->
bad_path_abort(
"Could not write runtime configuration due to reason: #{inspect(reason)}",
path
)
end
end
defp bad_path_abort(msg, path) do
abort(
msg <>
". Please make sure #{inspect(path)} is writable and accessible " <>
"or choose a different path"
)
end
defp abort(msg) do
IO.puts("ERROR! " <> msg)
:erlang.raise(:error, "aborting boot", [{Config.Provider, :boot, 2, []}])
end
end
-138
View File
@@ -1,138 +0,0 @@
defmodule Config.Reader do
@moduledoc """
API for reading config files defined with `Config`.
## As a provider
`Config.Reader` can also be used as a `Config.Provider`. A config
provider is used during releases to customize how applications are
configured. When used as a provider, it expects a single argument:
the configuration path (as outlined in `t:Config.Provider.config_path/0`)
for the file to be read and loaded during the system boot.
For example, if you expect the target system to have a config file
in an absolute path, you can add this inside the `def project` portion
of your `mix.exs`:
releases: [
demo: [
config_providers: [
{Config.Reader, "/etc/config.exs"}
]
]
]
Or if you want to read a custom path inside the release:
config_providers: [{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}
]
Remember Mix already loads `config/runtime.exs` by default.
For more examples and scenarios, see the `Config.Provider` module.
"""
@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)
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)
end
@doc """
Reads the configuration file.
## Options
* `:imports` - a list of already imported paths or `:disabled`
to disable imports
* `: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
"""
@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)
end
@doc """
Reads the given configuration file and returns the configuration
with its imports.
Accepts the same options as `read!/2`. Although note the `:imports`
option cannot be disabled in `read_imports!/2`.
"""
@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)
end
@doc """
Merges two configurations.
The configurations are merged together with the values in
the second one having higher preference than the first in
case of conflicts. In case both values are set to keyword
lists, it deep merges them.
## Examples
iex> Config.Reader.merge([app: [k: :v1]], [app: [k: :v2]])
[app: [k: :v2]]
iex> Config.Reader.merge([app: [k: [v1: 1, v2: 2]]], [app: [k: [v2: :a, v3: :b]]])
[app: [k: [v1: 1, v2: :a, v3: :b]]]
iex> Config.Reader.merge([app1: []], [app2: []])
[app1: [], app2: []]
"""
@doc since: "1.9.0"
@spec merge(keyword, keyword) :: keyword
def merge(config1, config2) when is_list(config1) and is_list(config2) do
Config.__merge__(config1, config2)
end
end
+50 -125
View File
@@ -1,6 +1,6 @@
defmodule Dict do
@moduledoc ~S"""
Generic API for dictionaries.
WARNING: this module is deprecated.
If you need a general dictionary, use the `Map` module.
If you need to manipulate keyword lists, use `Keyword`.
@@ -9,27 +9,19 @@ defmodule Dict do
`new` function in the respective modules.
"""
@moduledoc deprecated: "Use Map or Keyword modules instead"
@type key :: any
@type value :: any
@type t :: list | map
message =
"Use the Map module for working with maps or the Keyword module for working with keyword lists"
# TODO: Deprecate every function by 1.4
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), __CALLER__)
end
%{file: file, line: line} = __CALLER__
:elixir_errors.warn(line, file, "the Dict module is deprecated")
quote do
message = "Use maps and the Map module instead"
@deprecated message
def get(dict, key, default \\ nil) do
case fetch(dict, key) do
{:ok, value} -> value
@@ -37,7 +29,6 @@ defmodule Dict do
end
end
@deprecated message
def get_lazy(dict, key, fun) when is_function(fun, 0) do
case fetch(dict, key) do
{:ok, value} -> value
@@ -45,14 +36,12 @@ defmodule Dict do
end
end
@deprecated message
def get_and_update(dict, key, fun) do
current_value = get(dict, key)
{get, new_value} = fun.(current_value)
{get, put(dict, key, new_value)}
end
@deprecated message
def fetch!(dict, key) do
case fetch(dict, key) do
{:ok, value} -> value
@@ -60,35 +49,30 @@ defmodule Dict do
end
end
@deprecated message
def has_key?(dict, key) do
match?({:ok, _}, fetch(dict, key))
match? {:ok, _}, fetch(dict, key)
end
@deprecated message
def put_new(dict, key, value) do
case has_key?(dict, key) do
true -> dict
true -> dict
false -> put(dict, key, value)
end
end
@deprecated message
def put_new_lazy(dict, key, fun) when is_function(fun, 0) do
case has_key?(dict, key) do
true -> dict
true -> dict
false -> put(dict, key, fun.())
end
end
@deprecated message
def drop(dict, keys) do
Enum.reduce(keys, dict, &delete(&2, &1))
end
@deprecated message
def take(dict, keys) do
Enum.reduce(keys, new(), fn key, acc ->
Enum.reduce(keys, new, fn key, acc ->
case fetch(dict, key) do
{:ok, value} -> put(acc, key, value)
:error -> acc
@@ -96,49 +80,41 @@ defmodule Dict do
end)
end
@deprecated message
def to_list(dict) do
reduce(dict, {:cont, []}, fn kv, acc -> {:cont, [kv | acc]} end)
|> elem(1)
|> :lists.reverse()
reduce(dict, {:cont, []}, fn
kv, acc -> {:cont, [kv | acc]}
end) |> elem(1) |> :lists.reverse
end
@deprecated message
def keys(dict) do
reduce(dict, {:cont, []}, fn {k, _}, acc -> {:cont, [k | acc]} end)
|> elem(1)
|> :lists.reverse()
reduce(dict, {:cont, []}, fn
{k, _}, acc -> {:cont, [k | acc]}
end) |> elem(1) |> :lists.reverse
end
@deprecated message
def values(dict) do
reduce(dict, {:cont, []}, fn {_, v}, acc -> {:cont, [v | acc]} end)
|> elem(1)
|> :lists.reverse()
reduce(dict, {:cont, []}, fn
{_, v}, acc -> {:cont, [v | acc]}
end) |> elem(1) |> :lists.reverse
end
@deprecated message
def equal?(dict1, dict2) do
# Use this import to avoid conflicts in the user code
import Kernel, except: [size: 1]
case size(dict1) == size(dict2) do
false ->
false
true ->
reduce(dict1, {:cont, true}, fn {k, v}, _acc ->
false -> false
true ->
reduce(dict1, {:cont, true}, fn({k, v}, _acc) ->
case fetch(dict2, k) do
{:ok, ^v} -> {:cont, true}
_ -> {:halt, false}
end
end)
|> elem(1)
end) |> elem(1)
end
end
@deprecated message
def merge(dict1, dict2, fun \\ fn _k, _v1, v2 -> v2 end) do
def merge(dict1, dict2, fun \\ fn(_k, _v1, v2) -> v2 end) do
# Use this import to avoid conflicts in the user code
import Kernel, except: [size: 1]
@@ -150,182 +126,144 @@ defmodule Dict do
reduce(dict2, {:cont, dict1}, fn {k, v2}, acc ->
{:cont, update(acc, k, v2, &fun.(k, &1, v2))}
end)
end
|> elem(1)
end |> elem(1)
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
@deprecated message
def update!(dict, key, fun) do
case fetch(dict, key) do
{:ok, value} ->
put(dict, key, fun.(value))
:error ->
raise KeyError, key: key, term: dict
end
end
@deprecated message
def pop(dict, key, default \\ nil) do
case fetch(dict, key) do
{:ok, value} ->
{value, delete(dict, key)}
:error ->
{default, dict}
end
end
@deprecated message
def pop_lazy(dict, key, fun) when is_function(fun, 0) do
case fetch(dict, key) do
{:ok, value} ->
{value, delete(dict, key)}
:error ->
{fun.(), dict}
end
end
@deprecated message
def split(dict, keys) do
Enum.reduce(keys, {new(), dict}, fn key, {inc, exc} = acc ->
Enum.reduce(keys, {new, dict}, fn key, {inc, exc} = acc ->
case fetch(exc, key) do
{:ok, value} ->
{put(inc, key, value), delete(exc, key)}
:error ->
acc
end
end)
end
defoverridable merge: 2,
merge: 3,
equal?: 2,
to_list: 1,
keys: 1,
values: 1,
take: 2,
drop: 2,
get: 2,
get: 3,
fetch!: 2,
has_key?: 2,
put_new: 3,
pop: 2,
pop: 3,
split: 2,
update: 4,
update!: 3,
get_and_update: 3,
get_lazy: 3,
pop_lazy: 3,
put_new_lazy: 3
defoverridable merge: 2, merge: 3, equal?: 2, to_list: 1, keys: 1,
values: 1, take: 2, drop: 2, get: 2, get: 3, fetch!: 2,
has_key?: 2, put_new: 3, pop: 2, pop: 3, split: 2,
update: 4, update!: 3, get_and_update: 3, get_lazy: 3,
pop_lazy: 3, put_new_lazy: 3
end
end
defmacrop target(dict) do
quote do
case unquote(dict) do
%module{} -> module
%{} -> Map
dict when is_list(dict) -> Keyword
dict -> unsupported_dict(dict)
%{__struct__: x} when is_atom(x) ->
x
%{} ->
Map
x when is_list(x) ->
Keyword
x ->
unsupported_dict(x)
end
end
end
@deprecated message
@spec keys(t) :: [key]
def keys(dict) do
target(dict).keys(dict)
end
@deprecated message
@spec values(t) :: [value]
def values(dict) do
target(dict).values(dict)
end
@deprecated message
@spec size(t) :: non_neg_integer
def size(dict) do
target(dict).size(dict)
end
@deprecated message
@spec has_key?(t, key) :: boolean
def has_key?(dict, key) do
target(dict).has_key?(dict, key)
end
@deprecated message
@spec get(t, key, value) :: value
def get(dict, key, default \\ nil) do
target(dict).get(dict, key, default)
end
@deprecated message
@spec get_lazy(t, key, (() -> value)) :: value
def get_lazy(dict, key, fun) do
target(dict).get_lazy(dict, key, fun)
end
@deprecated message
@spec get_and_update(t, key, (value -> {value, value})) :: {value, t}
def get_and_update(dict, key, fun) do
target(dict).get_and_update(dict, key, fun)
end
@deprecated message
@spec fetch(t, key) :: value
def fetch(dict, key) do
target(dict).fetch(dict, key)
end
@deprecated message
@spec fetch!(t, key) :: value
@spec fetch!(t, key) :: value | no_return
def fetch!(dict, key) do
target(dict).fetch!(dict, key)
end
@deprecated message
@spec put(t, key, value) :: t
def put(dict, key, val) do
target(dict).put(dict, key, val)
end
@deprecated message
@spec put_new(t, key, value) :: t
def put_new(dict, key, val) do
target(dict).put_new(dict, key, val)
end
@deprecated message
@spec put_new_lazy(t, key, (() -> value)) :: t
def put_new_lazy(dict, key, fun) do
target(dict).put_new_lazy(dict, key, fun)
end
@deprecated message
@spec delete(t, key) :: t
def delete(dict, key) do
target(dict).delete(dict, key)
end
@deprecated message
@spec merge(t, t) :: t
def merge(dict1, dict2) do
target1 = target(dict1)
@@ -334,11 +272,10 @@ defmodule Dict do
if target1 == target2 do
target1.merge(dict1, dict2)
else
do_merge(target1, dict1, dict2, fn _k, _v1, v2 -> v2 end)
do_merge(target1, dict1, dict2, fn(_k, _v1, v2) -> v2 end)
end
end
@deprecated message
@spec merge(t, t, (key, value, value -> value)) :: t
def merge(dict1, dict2, fun) do
target1 = target(dict1)
@@ -352,61 +289,51 @@ defmodule Dict do
end
defp do_merge(target1, dict1, dict2, fun) do
Enumerable.reduce(dict2, {:cont, dict1}, fn {k, v}, acc ->
{:cont, target1.update(acc, k, v, fn other -> fun.(k, other, v) end)}
end)
|> elem(1)
Enumerable.reduce(dict2, {:cont, dict1}, fn({k, v}, acc) ->
{:cont, target1.update(acc, k, v, fn(other) -> fun.(k, other, v) end)}
end) |> elem(1)
end
@deprecated message
@spec pop(t, key, value) :: {value, t}
def pop(dict, key, default \\ nil) do
target(dict).pop(dict, key, default)
end
@deprecated message
@spec pop_lazy(t, key, (() -> value)) :: {value, t}
def pop_lazy(dict, key, fun) do
target(dict).pop_lazy(dict, key, fun)
end
@deprecated message
@spec update!(t, key, (value -> value)) :: t
def update!(dict, key, fun) do
target(dict).update!(dict, key, fun)
end
@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
@spec split(t, [key]) :: {t, t}
def split(dict, keys) do
target(dict).split(dict, keys)
end
@deprecated message
@spec drop(t, [key]) :: t
def drop(dict, keys) do
target(dict).drop(dict, keys)
end
@deprecated message
@spec take(t, [key]) :: t
def take(dict, keys) do
target(dict).take(dict, keys)
end
@deprecated message
@spec empty(t) :: t
def empty(dict) do
target(dict).empty(dict)
end
@deprecated message
@spec equal?(t, t) :: boolean
def equal?(dict1, dict2) do
target1 = target(dict1)
@@ -417,20 +344,18 @@ defmodule Dict do
target1.equal?(dict1, dict2)
target1.size(dict1) == target2.size(dict2) ->
Enumerable.reduce(dict2, {:cont, true}, fn {k, v}, _acc ->
Enumerable.reduce(dict2, {:cont, true}, fn({k, v}, _acc) ->
case target1.fetch(dict1, k) do
{:ok, ^v} -> {:cont, true}
_ -> {:halt, false}
_ -> {:halt, false}
end
end)
|> elem(1)
end) |> elem(1)
true ->
false
end
end
@deprecated message
@spec to_list(t) :: list
def to_list(dict) do
target(dict).to_list(dict)
@@ -438,6 +363,6 @@ defmodule Dict do
@spec unsupported_dict(t) :: no_return
defp unsupported_dict(dict) do
raise ArgumentError, "unsupported dict: #{inspect(dict)}"
raise ArgumentError, "unsupported dict: #{inspect dict}"
end
end
File diff suppressed because it is too large Load Diff
+1056 -2968
View File
File diff suppressed because it is too large Load Diff
+233 -1121
View File
File diff suppressed because it is too large Load Diff
+359 -871
View File
File diff suppressed because it is too large Load Diff
+10 -28
View File
@@ -12,7 +12,7 @@ defmodule File.Stat do
* `size` - size of file in bytes.
* `type` - `:device | :directory | :regular | :other | :symlink`; the type of the
* `type` - `:device | :directory | :regular | :other`; the type of the
file.
* `access` - `:read | :write | :read_write | :none`; the current system
@@ -23,7 +23,7 @@ defmodule File.Stat do
* `mtime` - the last time the file was written.
* `ctime` - the interpretation of this time field depends on the operating
system. On Unix-like operating systems, it is the last time the file or the inode was changed.
system. On Unix, it is the last time the file or the inode was changed.
In Windows, it is the time of creation.
* `mode` - the file permissions.
@@ -35,17 +35,17 @@ defmodule File.Stat do
In Windows, the number indicates a drive as follows: 0 means A:, 1 means
B:, and so on.
* `minor_device` - only valid for character devices on Unix-like systems. In all other
* `minor_device` - only valid for character devices on Unix. In all other
cases, this field is zero.
* `inode` - gives the inode number. On non-Unix-like file systems, this field
* `inode` - gives the inode number. On non-Unix file systems, this field
will be zero.
* `uid` - indicates the owner of the file. Will be zero for non-Unix-like file
* `uid` - indicates the owner of the file. Will be zero for non-Unix file
systems.
* `gid` - indicates the group that owns the file. Will be zero for
non-Unix-like file systems.
non-Unix file systems.
The time type returned in `atime`, `mtime`, and `ctime` is dependent on the
time type set in options. `{:time, type}` where type can be `:local`,
@@ -53,32 +53,16 @@ defmodule File.Stat do
"""
record = Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
keys = :lists.map(&elem(&1, 0), record)
vals = :lists.map(&{&1, [], nil}, keys)
pairs = :lists.zip(keys, vals)
keys = :lists.map(&elem(&1, 0), record)
vals = :lists.map(&{&1, [], nil}, keys)
pairs = :lists.zip(keys, vals)
defstruct keys
@type t :: %__MODULE__{
size: non_neg_integer(),
type: :device | :directory | :regular | :other | :symlink,
access: :read | :write | :read_write | :none,
atime: :calendar.datetime() | integer(),
mtime: :calendar.datetime() | integer(),
ctime: :calendar.datetime() | integer(),
mode: non_neg_integer(),
links: non_neg_integer(),
major_device: non_neg_integer(),
minor_device: non_neg_integer(),
inode: non_neg_integer(),
uid: non_neg_integer(),
gid: non_neg_integer()
}
@type t :: %__MODULE__{}
@doc """
Converts a `File.Stat` struct to a `:file_info` record.
"""
@spec to_record(t()) :: :file.file_info()
def to_record(%File.Stat{unquote_splicing(pairs)}) do
{:file_info, unquote_splicing(vals)}
end
@@ -86,9 +70,7 @@ defmodule File.Stat do
@doc """
Converts a `:file_info` record into a `File.Stat`.
"""
@spec from_record(:file.file_info()) :: t()
def from_record(file_info)
def from_record({:file_info, unquote_splicing(vals)}) do
%File.Stat{unquote_splicing(pairs)}
end
+16 -107
View File
@@ -7,7 +7,7 @@ defmodule File.Stream do
* `path` - the file path
* `modes` - the file modes
* `raw` - a boolean indicating if bin functions should be used
* `line_or_bytes` - if reading should read lines or a given number of bytes
* `line_or_bytes` - if reading should read lines or a given amount of bytes
"""
@@ -22,12 +22,11 @@ defmodule File.Stream do
modes =
case raw do
true ->
case :lists.keyfind(:read_ahead, 1, modes) do
{:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
{:read_ahead, _} -> [:raw | modes]
false -> [:raw, :read_ahead | modes]
if :lists.keyfind(:read_ahead, 1, modes) == {:read_ahead, false} do
[:raw | modes]
else
[:raw, :read_ahead | modes]
end
false ->
modes
end
@@ -37,12 +36,11 @@ defmodule File.Stream do
defimpl Collectable do
def into(%{path: path, modes: modes, raw: raw} = stream) do
modes = for mode <- modes, mode not in [:read], do: mode
modes = for mode <- modes, not mode in [:read], do: mode
case :file.open(path, [:write | modes]) do
{:ok, device} ->
{:ok, into(device, stream, raw)}
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
@@ -52,16 +50,14 @@ defmodule File.Stream do
fn
:ok, {:cont, x} ->
case raw do
true -> IO.binwrite(device, x)
true -> IO.binwrite(device, x)
false -> IO.write(device, x)
end
:ok, :done ->
# If delayed_write option is used and the last write failed will
# MatchError here as {:error, _} is returned.
:ok = :file.close(device)
stream
:ok, :halt ->
# If delayed_write option is used and the last write failed will
# MatchError here as {:error, _} is returned.
@@ -71,55 +67,27 @@ defmodule File.Stream do
end
defimpl Enumerable do
@read_ahead_size 64 * 1024
def reduce(%{path: path, modes: modes, line_or_bytes: line_or_bytes, raw: raw}, acc, fun) do
start_fun = fn ->
case :file.open(path, read_modes(modes)) do
{:ok, device} ->
if :trim_bom in modes, do: trim_bom(device, raw) |> elem(0), else: device
modes = for mode <- modes, not mode in [:write, :append], do: mode
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
start_fun =
fn ->
case :file.open(path, modes) do
{:ok, device} -> device
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
end
next_fun =
case raw do
true -> &IO.each_binstream(&1, line_or_bytes)
true -> &IO.each_binstream(&1, line_or_bytes)
false -> &IO.each_stream(&1, line_or_bytes)
end
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
end
def count(%{path: path, modes: modes, line_or_bytes: :line} = stream) do
pattern = :binary.compile_pattern("\n")
counter = &count_lines(&1, path, pattern, read_function(stream), 0)
case File.open(path, read_modes(modes), counter) do
{:ok, count} ->
{:ok, count}
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
def count(%{path: path, line_or_bytes: bytes, raw: true, modes: modes}) do
case File.stat(path) do
{:ok, %{size: 0}} ->
{:error, __MODULE__}
{:ok, %{size: size}} ->
remainder = if rem(size, bytes) == 0, do: 0, else: 1
{:ok, div(size, bytes) + remainder - count_raw_bom(path, modes)}
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
def count(_stream) do
{:error, __MODULE__}
end
@@ -127,64 +95,5 @@ defmodule File.Stream do
def member?(_stream, _term) do
{:error, __MODULE__}
end
def slice(_stream) do
{:error, __MODULE__}
end
defp count_raw_bom(path, modes) do
if :trim_bom in modes do
File.open!(path, read_modes(modes), &(&1 |> trim_bom(true) |> elem(1)))
else
0
end
end
defp trim_bom(device, true) do
bom_length = device |> IO.binread(4) |> bom_length()
{:ok, new_pos} = :file.position(device, bom_length)
{device, new_pos}
end
defp trim_bom(device, false) do
# Or we read the bom in the correct amount or it isn't there
case bom_length(IO.read(device, 1)) do
0 ->
{:ok, _} = :file.position(device, 0)
{device, 0}
_ ->
{device, 1}
end
end
defp bom_length(<<239, 187, 191, _rest::binary>>), do: 3
defp bom_length(<<254, 255, _rest::binary>>), do: 2
defp bom_length(<<255, 254, _rest::binary>>), do: 2
defp bom_length(<<0, 0, 254, 255, _rest::binary>>), do: 4
defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4
defp bom_length(_binary), do: 0
defp read_modes(modes) do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
end
defp count_lines(device, path, pattern, read, count) do
case read.(device) do
data when is_binary(data) ->
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
:eof ->
count
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
end
end
+73 -468
View File
@@ -2,115 +2,9 @@ import Kernel, except: [round: 1]
defmodule Float do
@moduledoc """
Functions for working with floating-point numbers.
## Kernel functions
There are functions related to floating-point numbers on the `Kernel` module
too. Here is a list of them:
* `Kernel.round/1`: rounds a number to the nearest integer.
* `Kernel.trunc/1`: returns the integer part of a number.
## Known issues
There are some very well known problems with floating-point numbers
and arithmetic due to the fact most decimal fractions cannot be
represented by a floating-point binary and most operations are not exact,
but operate on approximations. Those issues are not specific
to Elixir, they are a property of floating point representation itself.
For example, the numbers 0.1 and 0.01 are two of them, what means the result
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
what happens in this case:
* The closest representable number to 0.1 is 0.1000000014
* The closest representable number to 0.01 is 0.0099999997
* Doing 0.1 * 0.1 should return 0.01, but because 0.1 is actually 0.1000000014,
the result is 0.010000000000000002, and because this is not the closest
representable number to 0.01, you'll get the wrong result for this operation
There are also other known problems like flooring or rounding numbers. See
`round/2` and `floor/2` for more details about them.
To learn more about floating-point arithmetic visit:
* [0.30000000000000004.com](http://0.30000000000000004.com/)
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
Functions for working with floating point numbers.
"""
import Bitwise
@power_of_2_to_52 4_503_599_627_370_496
@precision_range 0..15
@type precision_range :: 0..15
@min_finite then(<<0xFFEFFFFFFFFFFFFF::64>>, fn <<num::float>> -> num end)
@max_finite then(<<0x7FEFFFFFFFFFFFFF::64>>, fn <<num::float>> -> num end)
@doc """
Returns the maximum finite value for a float.
## Examples
iex> Float.max_finite()
1.7976931348623157e308
"""
def max_finite, do: @max_finite
@doc """
Returns the minimum finite value for a float.
## Examples
iex> Float.min_finite()
-1.7976931348623157e308
"""
def min_finite, do: @min_finite
@doc """
Computes `base` raised to power of `exponent`.
`base` must be a float and `exponent` can be any number.
However, if a negative base and a fractional exponent
are given, it raises `ArithmeticError`.
It always returns a float. See `Integer.pow/2` for
exponentiation that returns integers.
## Examples
iex> Float.pow(2.0, 0)
1.0
iex> Float.pow(2.0, 1)
2.0
iex> Float.pow(2.0, 10)
1024.0
iex> Float.pow(2.0, -1)
0.5
iex> Float.pow(2.0, -3)
0.125
iex> Float.pow(3.0, 1.5)
5.196152422706632
iex> Float.pow(-2.0, 3)
-8.0
iex> Float.pow(-2.0, 4)
16.0
iex> Float.pow(-1.0, 0.5)
** (ArithmeticError) bad argument in arithmetic expression
"""
@doc since: "1.12.0"
@spec pow(float, number) :: float
def pow(base, exponent) when is_float(base) and is_number(exponent),
do: :math.pow(base, exponent)
@doc """
Parses a binary into a float.
@@ -119,8 +13,7 @@ defmodule Float do
returned.
If the size of float exceeds the maximum size of `1.7976931348623157e+308`,
`:error` is returned even though the textual representation itself might be
well formed.
the `ArgumentError` exception is raised.
If you want to convert a string-formatted float directly to a float,
`String.to_float/1` can be used instead.
@@ -129,15 +22,15 @@ defmodule Float do
iex> Float.parse("34")
{34.0, ""}
iex> Float.parse("34.25")
{34.25, ""}
iex> Float.parse("56.5xyz")
{56.5, "xyz"}
iex> Float.parse("pi")
:error
iex> Float.parse("1.7976931348623159e+308")
:error
"""
@spec parse(binary) :: {float, binary} | :error
@@ -156,109 +49,66 @@ defmodule Float do
parse_unsigned(binary)
end
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_unsigned(rest, false, false, <<digit>>)
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9, do:
parse_unsigned(rest, false, false, <<digit>>)
defp parse_unsigned(binary) when is_binary(binary), do: :error
defp parse_unsigned(binary) when is_binary(binary), do:
:error
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9, do:
parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9, do:
parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
when exp_marker in 'eE' and digit in ?0..?9,
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc) when exp_marker in 'eE' and digit in ?0..?9, do:
parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
when exp_marker in 'eE' and sign in '-+' and digit in ?0..?9,
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc) when exp_marker in 'eE' and sign in '-+' and digit in ?0..?9, do:
parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
# rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, true = _e?, acc) do
:erlang.binary_to_float(add_dot(acc, dot?))
rescue
ArgumentError -> :error
else
float -> {float, rest}
end
defp parse_unsigned(rest, dot?, _e?, acc), do:
{:erlang.binary_to_float(add_dot(acc, dot?)), rest}
defp parse_unsigned(rest, dot?, false = _e?, acc),
do: {:erlang.binary_to_float(add_dot(acc, dot?)), rest}
defp add_dot(acc, true), do: acc
defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: acc <> ".0"
@doc """
Rounds a float to the largest number less than or equal to `num`.
Rounds a float to the largest integer less than or equal to `num`.
`floor/2` also accepts a precision to round a floating-point value down
`floor/2` also accepts a precision to round a floating point value down
to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a
conversion to decimal.
This function always returns a float. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
## Known issues
The behaviour of `floor/2` for floats can be surprising. For example:
iex> Float.floor(12.52, 2)
12.51
One may have expected it to floor to 12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 12.51999999,
which explains the behaviour above.
## Examples
iex> Float.floor(34.25)
34.0
iex> Float.floor(-56.5)
-57.0
iex> Float.floor(34.259, 2)
34.25
"""
@spec floor(float, precision_range) :: float
def floor(number, precision \\ 0)
def floor(number, 0) when is_float(number) do
:math.floor(number)
end
def floor(number, precision) when is_float(number) and precision in @precision_range do
round(number, precision, :floor)
end
def floor(number, precision) when is_float(number) do
raise ArgumentError, invalid_precision_message(precision)
@spec floor(float, 0..15) :: float
def floor(number, precision \\ 0) when is_float(number) and precision in 0..15 do
power = power_of_10(precision)
number = number * power
truncated = trunc(number)
variance = if number - truncated < 0, do: -1.0, else: 0.0
(truncated + variance) / power
end
@doc """
Rounds a float to the smallest integer greater than or equal to `num`.
`ceil/2` also accepts a precision to round a floating-point value down
`ceil/2` also accepts a precision to round a floating point value down
to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a
conversion to decimal.
The behaviour of `ceil/2` for floats can be surprising. For example:
iex> Float.ceil(-12.52, 2)
-12.51
One may have expected it to ceil to -12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as -12.51999999,
which explains the behaviour above.
This function always returns floats. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -266,302 +116,68 @@ defmodule Float do
iex> Float.ceil(34.25)
35.0
iex> Float.ceil(-56.5)
-56.0
iex> Float.ceil(34.251, 2)
34.26
"""
@spec ceil(float, precision_range) :: float
def ceil(number, precision \\ 0)
def ceil(number, 0) when is_float(number) do
:math.ceil(number)
end
def ceil(number, precision) when is_float(number) and precision in @precision_range do
round(number, precision, :ceil)
end
def ceil(number, precision) when is_float(number) do
raise ArgumentError, invalid_precision_message(precision)
@spec ceil(float, 0..15) :: float
def ceil(number, precision \\ 0) when is_float(number) and precision in 0..15 do
power = power_of_10(precision)
number = number * power
truncated = trunc(number)
variance = if number - truncated > 0, do: 1.0, else: 0.0
(truncated + variance) / power
end
@doc """
Rounds a floating-point value to an arbitrary number of fractional
digits (between 0 and 15).
The rounding direction always ties to half up. The operation is
performed on the binary floating point, without a conversion to decimal.
Rounds a floating point value to an arbitrary number of fractional digits
(between 0 and 15).
This function only accepts floats and always returns a float. Use
`Kernel.round/1` if you want a function that accepts both floats
and integers and always returns an integer.
## Known issues
The behaviour of `round/2` for floats can be surprising. For example:
iex> Float.round(5.5675, 3)
5.567
One may have expected it to round to the half up 5.568. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 5.567499999,
which explains the behaviour above. If you want exact rounding for decimals,
you must use a decimal library. The behaviour above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
`Kernel.round/1` if you want a function that accepts both floats and integers
and always returns an integer.
## Examples
iex> Float.round(12.5)
13.0
iex> Float.round(5.5674, 3)
5.567
iex> Float.round(5.5675, 3)
5.567
5.568
iex> Float.round(-5.5674, 3)
-5.567
iex> Float.round(-5.5675, 3)
-5.568
iex> Float.round(-5.5675)
-6.0
iex> Float.round(12.341444444444441, 15)
12.341444444444441
"""
@spec round(float, precision_range) :: float
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
def round(float, precision \\ 0)
def round(float, 0) when is_float(float) do
float |> :erlang.round() |> :erlang.float()
@spec round(float, 0..15) :: float
def round(number, precision \\ 0) when is_float(number) and precision in 0..15 do
power = power_of_10(precision)
Kernel.round(number * power) / power
end
def round(float, precision) when is_float(float) and precision in @precision_range do
round(float, precision, :half_up)
end
def round(float, precision) when is_float(float) do
raise ArgumentError, invalid_precision_message(precision)
end
defp round(0.0 = num, _precision, _rounding), do: num
defp round(float, precision, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
cond do
# Precision beyond 15 digits
count >= 104 ->
case rounding do
:ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
_ -> 0.0
end
# We are asking more precision than we have
count <= precision ->
float
true ->
# Difference in precision between float and asked precision
# We subtract 1 because we need to calculate the remainder too
diff = count - precision - 1
# Get up to latest so we calculate the remainder
power_of_10 = power_of_10(diff)
# Convert the numerand to decimal base
num = num * power_of_5(count)
# Move to the given precision - 1
num = div(num, power_of_10)
div = div(num, 10)
num = rounding(rounding, sign, num, div)
# Convert back to float without loss
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
den = power_of_10(precision)
boundary = den <<< 52
cond do
num == 0 ->
0.0
num >= boundary ->
{den, exp} = scale_down(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
true ->
{num, exp} = scale_up(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
end
end
end
defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
end
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
end
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, last_count, acc)
end
defp decompose(<<>>, _count, last_count, acc) do
{acc, last_count}
end
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
defp scale_down(num, den, exp) do
new_den = den <<< 1
if num < new_den do
{den >>> 52, exp}
else
scale_down(num, new_den, exp + 1)
end
end
defp decimal_to_float(sign, num, den, exp) do
quo = div(num, den)
rem = num - quo * den
tmp =
case den >>> 1 do
den when rem > den -> quo + 1
den when rem < den -> quo
_ when (quo &&& 1) === 1 -> quo + 1
_ -> quo
end
tmp = tmp - @power_of_2_to_52
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
tmp
end
defp rounding(:floor, 1, _num, div), do: div + 1
defp rounding(:ceil, 0, _num, div), do: div + 1
defp rounding(:half_up, _sign, num, div) do
case rem(num, 10) do
rem when rem < 5 -> div
rem when rem >= 5 -> div + 1
end
end
defp rounding(_, _, _, div), do: div
Enum.reduce(0..104, 1, fn x, acc ->
Enum.reduce 0..15, 1, fn x, acc ->
defp power_of_10(unquote(x)), do: unquote(acc)
acc * 10
end)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end)
@doc """
Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator.
## Examples
iex> Float.ratio(0.0)
{0, 1}
iex> Float.ratio(3.14)
{7070651414971679, 2251799813685248}
iex> Float.ratio(-3.14)
{-7070651414971679, 2251799813685248}
iex> Float.ratio(1.5)
{3, 2}
iex> Float.ratio(-1.5)
{-3, 2}
iex> Float.ratio(16.0)
{16, 1}
iex> Float.ratio(-16.0)
{-16, 1}
"""
@doc since: "1.4.0"
@spec ratio(float) :: {integer, pos_integer}
def ratio(0.0), do: {0, 1}
def ratio(float) when is_float(float) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
{num, den_exp} =
if exp != 0 do
# Floats are expressed like this:
# (2**52 + mantissa) * 2**(-52 + exp - 1023)
#
# We compute the root factors of the mantissa so we have this:
# (2**52 + mantissa * 2**count) * 2**(-52 + exp - 1023)
{mantissa, count} = root_factors(mantissa, 0)
# Now we can move the count around so we have this:
# (2**(52-count) + mantissa) * 2**(count + -52 + exp - 1023)
if mantissa == 0 do
{1, exp - 1023}
else
num = (1 <<< (52 - count)) + mantissa
den_exp = count - 52 + exp - 1023
{num, den_exp}
end
else
# Subnormals are expressed like this:
# (mantissa) * 2**(-52 + 1 - 1023)
#
# So we compute it to this:
# (mantissa * 2**(count)) * 2**(-52 + 1 - 1023)
#
# Which becomes:
# mantissa * 2**(count-1074)
root_factors(mantissa, -1074)
end
if den_exp > 0 do
{sign(sign, num <<< den_exp), 1}
else
{sign(sign, num), 1 <<< -den_exp}
end
end
defp root_factors(mantissa, count) when mantissa != 0 and (mantissa &&& 1) == 0,
do: root_factors(mantissa >>> 1, count + 1)
defp root_factors(mantissa, count),
do: {mantissa, count}
@compile {:inline, sign: 2}
defp sign(0, num), do: num
defp sign(1, num), do: -num
@doc """
Returns a charlist which corresponds to the shortest text representation
Returns a charlist which corresponds to the text representation
of the given float.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`.
It uses the shortest representation according to algorithm described
in "Printing Floating-Point Numbers Quickly and Accurately" in
Proceedings of the SIGPLAN '96 Conference on Programming Language
Design and Implementation.
## Examples
@@ -575,20 +191,13 @@ defmodule Float do
end
@doc """
Returns a binary which corresponds to the shortest text representation
Returns a binary which corresponds to the text representation
of the given float.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_binary/2`.
It uses the shortest representation according to algorithm described
in "Printing Floating-Point Numbers Quickly and Accurately" in
Proceedings of the SIGPLAN '96 Conference on Programming Language
Design and Implementation.
## Examples
@@ -596,33 +205,29 @@ defmodule Float do
"7.0"
"""
@spec to_string(float) :: String.t()
@spec to_string(float) :: String.t
def to_string(float) when is_float(float) do
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
end
# TODO: Deprecate by v1.5
@doc false
@deprecated "Use Float.to_charlist/1 instead"
def to_char_list(float), do: Float.to_charlist(float)
# TODO: Deprecate by v1.4
@doc false
@deprecated "Use :erlang.float_to_list/2 instead"
def to_char_list(float, options) do
:erlang.float_to_list(float, expand_compact(options))
end
# TODO: Deprecate by v1.4
@doc false
@deprecated "Use :erlang.float_to_binary/2 instead"
def to_string(float, options) do
:erlang.float_to_binary(float, expand_compact(options))
end
defp invalid_precision_message(precision) do
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
end
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
defp expand_compact([{:compact, true} | t]), do: [:compact | expand_compact(t)]
defp expand_compact([h | t]), do: [h | expand_compact(t)]
defp expand_compact([]), do: []
defp expand_compact([{:compact, true} | t]), do: [:compact | expand_compact(t)]
defp expand_compact([h | t]), do: [h | expand_compact(t)]
defp expand_compact([]), do: []
end
-208
View File
@@ -1,208 +0,0 @@
defmodule Function do
@moduledoc """
A set of functions for working with functions.
Anonymous functions are typically created by using `fn`:
iex> add = fn a, b -> a + b end
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 `&/1`:
iex> add = &Kernel.+/2
iex> add.(1, 2)
3
iex> length = &String.length/1
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`).
The capture operator can also be used to create anonymous functions
that expect at least one argument:
iex> add = &(&1 + &2)
iex> add.(1, 2)
3
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.
Besides the functions in this module to work with functions, `Kernel` also
has an `apply/2` function that invokes a function with a dynamic number of
arguments, as well as `is_function/1` and `is_function/2`, to check
respectively if a given value is a function or a function of a given arity.
"""
@type information ::
:arity
| :env
| :index
| :module
| :name
| :new_index
| :new_uniq
| :pid
| :type
| :uniq
@doc """
Captures the given function.
Inlined by the compiler.
## Examples
iex> Function.capture(String, :length, 1)
&String.length/1
"""
@doc since: "1.7.0"
@spec capture(module, atom, arity) :: fun
def capture(module, function_name, arity) do
:erlang.make_fun(module, function_name, arity)
end
@doc """
Returns a keyword list with information about a function.
The returned keys (with the corresponding possible values) for
all types of functions (local and external) are the following:
* `:type` - `:local` (for anonymous functions) or `:external` (for
named functions).
* `:module` - an atom which is the module where the function is defined when
anonymous or the module which the function refers to when it's a named function.
* `:arity` - (integer) the number of arguments the function is to be called with.
* `:name` - (atom) the name of the function.
* `:env` - a list of the environment or free variables. For named
functions, the returned list is always empty.
When `fun` is an anonymous function (that is, the type is `:local`), the following
additional keys are returned:
* `:pid` - PID of the process that originally created the function.
* `:index` - (integer) an index into the module function table.
* `:new_index` - (integer) an index into the module function table.
* `:new_uniq` - (binary) a unique value for this function. It's
calculated from the compiled code for the entire module.
* `:uniq` - (integer) a unique value for this function. This integer is
calculated from the compiled code for the entire module.
**Note**: this function must be used only for debugging purposes.
Inlined by the compiler.
## Examples
iex> fun = fn x -> x end
iex> info = Function.info(fun)
iex> Keyword.get(info, :arity)
1
iex> Keyword.get(info, :type)
:local
iex> fun = &String.length/1
iex> info = Function.info(fun)
iex> Keyword.get(info, :type)
:external
iex> Keyword.get(info, :name)
:length
"""
@doc since: "1.7.0"
@spec info(fun) :: [{information, term}]
def info(fun), do: :erlang.fun_info(fun)
@doc """
Returns a specific information about the function.
The returned information is a two-element tuple in the shape of
`{info, value}`.
For any function, the information asked for can be any of the atoms
`:module`, `:name`, `:arity`, `:env`, or `:type`.
For anonymous functions, there is also information about any of the
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
For a named function, the value of any of these items is always the
atom `:undefined`.
For more information on each of the possible returned values, see
`info/1`.
Inlined by the compiler.
## Examples
iex> f = fn x -> x end
iex> Function.info(f, :arity)
{:arity, 1}
iex> Function.info(f, :type)
{:type, :local}
iex> fun = &String.length/1
iex> Function.info(fun, :name)
{:name, :length}
iex> Function.info(fun, :pid)
{:pid, :undefined}
"""
@doc since: "1.7.0"
@spec info(fun, item) :: {item, term} when item: information
def info(fun, item), do: :erlang.fun_info(fun, item)
@doc """
Returns its input `value`. This function can be passed as an anonymous function
to transformation functions.
## Examples
iex> Function.identity("Hello world!")
"Hello world!"
iex> 'abcdaabccc' |> Enum.sort() |> Enum.chunk_by(&Function.identity/1)
['aaa', 'bb', 'cccc', 'd']
iex> Enum.group_by('abracadabra', &Function.identity/1)
%{97 => 'aaaaa', 98 => 'bb', 99 => 'c', 100 => 'd', 114 => 'rr'}
iex> Enum.map([1, 2, 3, 4], &Function.identity/1)
[1, 2, 3, 4]
"""
@doc since: "1.10.0"
@spec identity(value) :: value when value: var
def identity(value), do: value
end
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -1,8 +1,19 @@
defmodule GenEvent.Stream do
@moduledoc false
@moduledoc """
Defines a `GenEvent` stream.
This is a struct returned by `GenEvent.stream/2`. The struct is public and
contains the following fields:
* `:manager` - the manager reference given to `GenEvent.stream/2`
* `:timeout` - the timeout between events, defaults to `:infinity`
"""
defstruct manager: nil, timeout: :infinity
@type t :: %__MODULE__{manager: GenEvent.manager(), timeout: timeout}
@type t :: %__MODULE__{
manager: GenEvent.manager,
timeout: timeout}
@doc false
def init({_pid, _ref} = state) do
@@ -22,7 +33,6 @@ defmodule GenEvent.Stream do
def handle_call(msg, _state) do
# We do this to trick Dialyzer to not complain about non-local returns.
reason = {:bad_call, msg}
case :erlang.phash2(1, 1) do
0 -> exit(reason)
1 -> {:remove_handler, reason}
@@ -47,7 +57,7 @@ end
defimpl Enumerable, for: GenEvent.Stream do
def reduce(stream, acc, fun) do
start_fun = fn -> start(stream) end
start_fun = fn() -> start(stream) end
next_fun = &next(stream, &1)
stop_fun = &stop(stream, &1)
Stream.resource(start_fun, next_fun, stop_fun).(acc, wrap_reducer(fun))
@@ -61,33 +71,26 @@ defimpl Enumerable, for: GenEvent.Stream do
{:error, __MODULE__}
end
def slice(_stream) do
{:error, __MODULE__}
end
defp wrap_reducer(fun) do
fn
{:ack, manager, ref, event}, acc ->
send(manager, {ref, :ok})
send manager, {ref, :ok}
fun.(event, acc)
{:async, _manager, _ref, event}, acc ->
fun.(event, acc)
{:sync, manager, ref, event}, acc ->
try do
fun.(event, acc)
after
send(manager, {ref, :ok})
send manager, {ref, :ok}
end
end
end
defp start(%{manager: manager} = stream) do
try do
{:ok, {pid, ref}} =
:gen.call(manager, self(), {:add_process_handler, self(), self()}, :infinity)
{:ok, {pid, ref}} = :gen.call(manager, self(),
{:add_process_handler, self(), self()}, :infinity)
mon_ref = Process.monitor(pid)
{pid, ref, mon_ref}
catch
@@ -135,7 +138,6 @@ defimpl Enumerable, for: GenEvent.Stream do
case wait_for_handler_removal(pid, ref, mon_ref) do
:ok ->
flush_events(ref)
{:error, reason} ->
exit({reason, {__MODULE__, :stop, [stream, acc]}})
end
@@ -144,7 +146,7 @@ defimpl Enumerable, for: GenEvent.Stream do
# If we reach this branch, the handler was not removed yet,
# so we trigger a request for doing so.
defp stop(stream, {pid, ref, _} = acc) do
_ = :gen_event.delete_handler(pid, {pid, ref}, :shutdown)
_ = GenEvent.remove_handler(pid, {pid, ref}, :shutdown)
stop(stream, {:removed, acc})
end
@@ -153,7 +155,6 @@ defimpl Enumerable, for: GenEvent.Stream do
{:gen_event_EXIT, {^pid, ^ref}, _reason} ->
Process.demonitor(mon_ref, [:flush])
:ok
{:DOWN, ^mon_ref, _, _, reason} ->
{:error, reason}
end
@@ -161,8 +162,7 @@ defimpl Enumerable, for: GenEvent.Stream do
defp flush_events(ref) do
receive do
{_from, {_pid, ^ref}, {notify, _event}}
when notify in [:notify, :ack_notify, :sync_notify] ->
{_from, {_pid, ^ref}, {notify, _event}} when notify in [:notify, :ack_notify, :sync_notify] ->
flush_events(ref)
after
0 -> :ok
File diff suppressed because it is too large Load Diff
+31 -95
View File
@@ -1,11 +1,11 @@
defmodule HashDict do
@moduledoc """
Tuple-based HashDict implementation.
WARNING: this module is deprecated.
This module is deprecated. Use the `Map` module instead.
Use the `Map` module instead.
"""
@moduledoc deprecated: "Use Map instead"
# TODO: Deprecate every function by 1.4
use Dict
@@ -22,70 +22,58 @@ defmodule HashDict do
@compile :inline_list_funcs
@compile {:inline, key_hash: 1, key_mask: 1, key_shift: 1}
message = "Use maps and the Map module instead"
@doc """
Creates a new empty dict.
"""
@spec new :: Dict.t()
@deprecated message
@spec new :: Dict.t
def new do
%HashDict{}
end
@deprecated message
def put(%HashDict{root: root, size: size}, key, value) do
{root, counter} = do_put(root, key, value, key_hash(key))
%HashDict{root: root, size: size + counter}
end
@deprecated message
def update!(%HashDict{root: root, size: size} = dict, key, fun) when is_function(fun, 1) do
{root, counter} =
do_update(root, key, fn -> raise KeyError, key: key, term: dict end, fun, key_hash(key))
{root, counter} = do_update(root, key, fn -> raise KeyError, key: key, term: dict end,
fun, key_hash(key))
%HashDict{root: root, size: size + counter}
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
@deprecated message
def fetch(%HashDict{root: root}, key) do
do_fetch(root, key, key_hash(key))
end
@deprecated message
def delete(dict, key) do
case dict_delete(dict, key) do
{dict, _value} -> dict
:error -> dict
:error -> dict
end
end
@deprecated message
def pop(dict, key, default \\ nil) do
case dict_delete(dict, key) do
{dict, value} -> {value, dict}
:error -> {default, dict}
:error -> {default, dict}
end
end
@deprecated message
def size(%HashDict{size: size}) do
size
end
@doc false
@deprecated message
def reduce(%HashDict{root: root}, acc, fun) do
do_reduce(root, acc, fun, @node_size, fn
{:suspend, acc} -> {:suspended, acc, &{:done, elem(&1, 1)}}
{:halt, acc} -> {:halted, acc}
{:cont, acc} -> {:done, acc}
{:halt, acc} -> {:halted, acc}
{:cont, acc} -> {:done, acc}
end)
end
@@ -95,7 +83,7 @@ defmodule HashDict do
def dict_delete(%HashDict{root: root, size: size}, key) do
case do_delete(root, key, key_hash(key)) do
{root, value} -> {%HashDict{root: root, size: size - 1}, value}
:error -> :error
:error -> :error
end
end
@@ -103,92 +91,74 @@ defmodule HashDict do
defp do_fetch(node, key, hash) do
index = key_mask(hash)
case elem(node, index) do
[^key | v] -> {:ok, v}
[^key | v] -> {:ok, v}
{^key, v, _} -> {:ok, v}
{_, _, n} -> do_fetch(n, key, key_shift(hash))
_ -> :error
{_, _, n} -> do_fetch(n, key, key_shift(hash))
_ -> :error
end
end
defp do_put(node, key, value, hash) do
index = key_mask(hash)
case elem(node, index) do
[] ->
{put_elem(node, index, [key | value]), 1}
[^key | _] ->
{put_elem(node, index, [key | value]), 0}
[k | v] ->
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | value])
{put_elem(node, index, {k, v, n}), 1}
{^key, _, n} ->
{put_elem(node, index, {key, value, n}), 0}
{k, v, n} ->
{n, counter} = do_put(n, key, value, key_shift(hash))
{put_elem(node, index, {k, v, n}), counter}
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
defp do_delete(node, key, hash) do
index = key_mask(hash)
case elem(node, index) do
[] ->
:error
[^key | value] ->
{put_elem(node, index, []), value}
[_ | _] ->
:error
{^key, value, n} ->
{put_elem(node, index, do_compact_node(n)), value}
{k, v, n} ->
case do_delete(n, key, key_shift(hash)) do
{@node_template, value} ->
{put_elem(node, index, [k | v]), value}
{n, value} ->
{put_elem(node, index, {k, v, n}), value}
:error ->
:error
end
end
end
Enum.each(0..(@node_size - 1), fn index ->
Enum.each 0..(@node_size - 1), fn index ->
defp do_compact_node(node) when elem(node, unquote(index)) != [] do
case elem(node, unquote(index)) do
[k | v] ->
@@ -196,12 +166,11 @@ defmodule HashDict do
@node_template -> [k | v]
n -> {k, v, n}
end
{k, v, n} ->
{k, v, put_elem(node, unquote(index), do_compact_node(n))}
end
end
end)
end
## Dict reduce
@@ -226,12 +195,7 @@ defmodule HashDict do
end
defp do_reduce(node, acc, fun, count, next) when count > 0 do
do_reduce_each(
:erlang.element(count, node),
acc,
fun,
&do_reduce(node, &1, fun, count - 1, next)
)
do_reduce_each(:erlang.element(count, node), acc, fun, &do_reduce(node, &1, fun, count - 1, next))
end
defp do_reduce(_node, acc, _fun, 0, next) do
@@ -256,45 +220,19 @@ defmodule HashDict do
end
defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
def member?(_dict, _) do
{:ok, false}
end
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
{:ok, module.size(dict)}
end
def slice(_dict) do
{:error, __MODULE__}
end
def reduce(dict, acc, fun), do: HashDict.reduce(dict, acc, fun)
def member?(dict, {k, v}), do: {:ok, match?({:ok, ^v}, HashDict.fetch(dict, k))}
def member?(_dict, _), do: {:ok, false}
def count(dict), do: {:ok, HashDict.size(dict)}
end
defimpl Collectable, for: HashDict do
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
{original, fn
dict, {:cont, {k, v}} -> HashDict.put(dict, k, v)
dict, :done -> dict
_, :halt -> :ok
end
{original, collector_fun}
end}
end
end
@@ -302,8 +240,6 @@ defimpl Inspect, for: HashDict do
import Inspect.Algebra
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
concat ["#HashDict<", Inspect.List.inspect(HashDict.to_list(dict), opts), ">"]
end
end
+44 -104
View File
@@ -1,19 +1,17 @@
defmodule HashSet do
@moduledoc """
Tuple-based HashSet implementation.
WARNING: this module is deprecated.
This module is deprecated. Use the `MapSet` module instead.
Use the `MapSet` module instead.
"""
@moduledoc deprecated: "Use MapSet instead"
# TODO: Deprecate every function by 1.4
@node_bitmap 0b111
@node_shift 3
@node_size 8
@node_template :erlang.make_tuple(@node_size, [])
message = "Use the MapSet module instead"
@opaque t :: %__MODULE__{size: non_neg_integer, root: term}
@doc false
defstruct size: 0, root: @node_template
@@ -22,85 +20,71 @@ defmodule HashSet do
@compile :inline_list_funcs
@compile {:inline, key_hash: 1, key_mask: 1, key_shift: 1}
@deprecated message
@spec new :: Set.t()
@spec new :: Set.t
def new do
%HashSet{}
end
@deprecated message
def union(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) when size1 <= size2 do
set_fold(set1, set2, fn v, acc -> put(acc, v) end)
set_fold set1, set2, fn v, acc -> put(acc, v) end
end
@deprecated message
def union(%HashSet{} = set1, %HashSet{} = set2) do
set_fold(set2, set1, fn v, acc -> put(acc, v) end)
set_fold set2, set1, fn v, acc -> put(acc, v) end
end
@deprecated message
def intersection(%HashSet{} = set1, %HashSet{} = set2) do
set_fold(set1, %HashSet{}, fn v, acc ->
set_fold set1, %HashSet{}, fn v, acc ->
if member?(set2, v), do: put(acc, v), else: acc
end)
end
@deprecated message
def difference(%HashSet{} = set1, %HashSet{} = set2) do
set_fold(set2, set1, fn v, acc -> delete(acc, v) end)
end
@deprecated message
def to_list(set) do
set_fold(set, [], &[&1 | &2]) |> :lists.reverse()
end
@deprecated message
def equal?(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) do
case size1 do
^size2 -> subset?(set1, set2)
_ -> false
end
end
@deprecated message
def difference(%HashSet{} = set1, %HashSet{} = set2) do
set_fold set2, set1, fn v, acc -> delete(acc, v) end
end
def to_list(set) do
set_fold(set, [], &[&1 | &2]) |> :lists.reverse
end
def equal?(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) do
case size1 do
^size2 -> subset?(set1, set2)
_ -> false
end
end
def subset?(%HashSet{} = set1, %HashSet{} = set2) do
reduce(set1, {:cont, true}, fn member, acc ->
case member?(set2, member) do
true -> {:cont, acc}
_ -> {:halt, false}
_ -> {:halt, false}
end
end)
|> elem(1)
end) |> elem(1)
end
@deprecated message
def disjoint?(%HashSet{} = set1, %HashSet{} = set2) do
reduce(set2, {:cont, true}, fn member, acc ->
case member?(set1, member) do
false -> {:cont, acc}
_ -> {:halt, false}
_ -> {:halt, false}
end
end)
|> elem(1)
end) |> elem(1)
end
@deprecated message
def member?(%HashSet{root: root}, term) do
do_member?(root, term, key_hash(term))
end
@deprecated message
def put(%HashSet{root: root, size: size}, term) do
{root, counter} = do_put(root, term, key_hash(term))
%HashSet{root: root, size: size + counter}
end
@deprecated message
def delete(%HashSet{root: root, size: size} = set, term) do
case do_delete(root, term, key_hash(term)) do
{:ok, root} -> %HashSet{root: root, size: size - 1}
:error -> set
:error -> set
end
end
@@ -108,12 +92,11 @@ defmodule HashSet do
def reduce(%HashSet{root: root}, acc, fun) do
do_reduce(root, acc, fun, @node_size, fn
{:suspend, acc} -> {:suspended, acc, &{:done, elem(&1, 1)}}
{:halt, acc} -> {:halted, acc}
{:cont, acc} -> {:done, acc}
{:halt, acc} -> {:halted, acc}
{:cont, acc} -> {:done, acc}
end)
end
@deprecated message
def size(%HashSet{size: size}) do
size
end
@@ -128,29 +111,24 @@ defmodule HashSet do
defp do_member?(node, term, hash) do
index = key_mask(hash)
case elem(node, index) do
[] -> false
[] -> false
[^term | _] -> true
[_] -> false
[_ | n] -> do_member?(n, term, key_shift(hash))
[_] -> false
[_ | n] -> do_member?(n, term, key_shift(hash))
end
end
defp do_put(node, term, hash) do
index = key_mask(hash)
case elem(node, index) do
[] ->
{put_elem(node, index, [term]), 1}
[^term | _] ->
{node, 0}
[t] ->
n = put_elem(@node_template, key_mask(key_shift(hash)), [term])
{put_elem(node, index, [t | n]), 1}
[t | n] ->
{n, counter} = do_put(n, term, key_shift(hash))
{put_elem(node, index, [t | n]), counter}
@@ -159,35 +137,28 @@ defmodule HashSet do
defp do_delete(node, term, hash) do
index = key_mask(hash)
case elem(node, index) do
[] ->
:error
[^term] ->
{:ok, put_elem(node, index, [])}
[_] ->
:error
[^term | n] ->
{:ok, put_elem(node, index, do_compact_node(n))}
[t | n] ->
case do_delete(n, term, key_shift(hash)) do
{:ok, @node_template} ->
{:ok, put_elem(node, index, [t])}
{:ok, n} ->
{:ok, put_elem(node, index, [t | n])}
:error ->
:error
end
end
end
Enum.each(0..(@node_size - 1), fn index ->
Enum.each 0..(@node_size - 1), fn index ->
defp do_compact_node(node) when elem(node, unquote(index)) != [] do
case elem(node, unquote(index)) do
[t] ->
@@ -195,17 +166,16 @@ defmodule HashSet do
@node_template -> [t]
n -> [t | n]
end
[t | n] ->
[t | put_elem(node, unquote(index), do_compact_node(n))]
end
end
end)
end
## Set fold
defp do_fold_each([], acc, _fun), do: acc
defp do_fold_each([t], acc, fun), do: fun.(t, acc)
defp do_fold_each([], acc, _fun), do: acc
defp do_fold_each([t], acc, fun), do: fun.(t, acc)
defp do_fold_each([t | n], acc, fun), do: do_fold(n, fun.(t, acc), fun, @node_size)
defp do_fold(node, acc, fun, count) when count > 0 do
@@ -240,12 +210,7 @@ defmodule HashSet do
end
defp do_reduce(node, acc, fun, count, next) when count > 0 do
do_reduce_each(
:erlang.element(count, node),
acc,
fun,
&do_reduce(node, &1, fun, count - 1, next)
)
do_reduce_each(:erlang.element(count, node), acc, fun, &do_reduce(node, &1, fun, count - 1, next))
end
defp do_reduce(_node, acc, _fun, 0, next) do
@@ -270,41 +235,18 @@ defmodule HashSet do
end
defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
{:ok, module.size(set)}
end
def slice(_set) do
{:error, __MODULE__}
end
def reduce(set, acc, fun), do: HashSet.reduce(set, acc, fun)
def member?(set, v), do: {:ok, HashSet.member?(set, v)}
def count(set), do: {:ok, HashSet.size(set)}
end
defimpl Collectable, for: HashSet do
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
collector_fun = fn
set, {:cont, term} -> module.put(set, term)
{original, fn
set, {:cont, x} -> HashSet.put(set, x)
set, :done -> set
_, :halt -> :ok
end
{original, collector_fun}
end}
end
end
@@ -312,8 +254,6 @@ defimpl Inspect, for: HashSet do
import Inspect.Algebra
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
concat ["#HashSet<", Inspect.List.inspect(HashSet.to_list(set), opts), ">"]
end
end
+336 -434
View File
@@ -1,172 +1,149 @@
import Kernel, except: [inspect: 1]
import Inspect.Algebra
alias Code.Identifier
defprotocol Inspect do
@moduledoc """
The `Inspect` protocol converts an Elixir data structure into an
algebra document.
The `Inspect` protocol is responsible for converting any Elixir
data structure into an algebra document. This document is then
formatted, either in pretty printing format or a regular one.
This is typically done when you want to customize how your own
structs are inspected in logs and the terminal.
The `inspect/2` function receives the entity to be inspected
followed by the inspecting options, represented by the struct
`Inspect.Opts`.
This documentation refers to implementing the `Inspect` protocol
for your own data structures. To learn more about using inspect,
see `Kernel.inspect/2` and `IO.inspect/2`.
Inspection is done using the functions available in `Inspect.Algebra`.
## Inspect representation
There are typically three choices of inspect representation. In order
to understand them, let's imagine we have the following `User` struct:
defmodule User do
defstruct [:id, :name, :address]
end
Our choices are:
1. Print the struct using Elixir's struct syntax, for example:
`%User{address: "Earth", id: 13, name: "Jane"}`. This is the
default representation and best choice if all struct fields
are public.
2. Print using the `#User<...>` notation, for example: `#User<id: 13, name: "Jane", ...>`.
This notation does not emit valid Elixir code and is typically
used when the struct has private fields (for example, you may want
to hide the field `:address` to redact person identifiable information).
3. Print the struct using the expression syntax, for example:
`User.new(13, "Jane", "Earth")`. This assumes there is a `User.new/3`
function. This option is mostly used as an alternative to option 2
for representing custom data structures, such as `MapSet`, `Date.Range`,
and others.
You can implement the Inspect protocol for your own structs while
adhering to the conventions above. Option 1 is the default representation
and you can quickly achieve option 2 by deriving the `Inspect` protocol.
For option 3, you need your custom implementation.
## Deriving
The `Inspect` protocol can be derived to customize the order of fields
(the default is alphabetical) and hide certain fields from structs,
so they don't show up in logs, inspects and similar. The latter is
especially useful for fields containing private information.
The supported options are:
* `:only` - only include the given fields when inspecting.
* `:except` - remove the given fields when inspecting.
* `:optional` - (since v1.14.0) do not include a field if it
matches its default value. This can be used to simplify the
struct representation at the cost of hiding information.
Whenever `:only` or `:except` are used to restrict fields,
the struct will be printed using the `#User<...>` notation,
as the struct can no longer be copy and pasted as valid Elixir
code. Let's see an example:
defmodule User do
@derive {Inspect, only: [:id, :name]}
defstruct [:id, :name, :address]
end
inspect(%User{id: 1, name: "Jane", address: "Earth"})
#=> #User<id: 1, name: "Jane", ...>
If you use only the `:optional` option, the struct will still be
printed as `%User{...}`.
## Custom implementation
You can also define your custom protocol implementation by
defining the `inspect/2` function. The function receives the
entity to be inspected followed by the inspecting options,
represented by the struct `Inspect.Opts`. Building of the
algebra document is done with `Inspect.Algebra`.
## Examples
Many times, inspecting a structure can be implemented in function
of existing entities. For example, here is `MapSet`'s `inspect/2`
of existing entities. For example, here is `MapSet`'s `inspect`
implementation:
defimpl Inspect, for: MapSet do
import Inspect.Algebra
def inspect(map_set, opts) do
concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"])
def inspect(dict, opts) do
concat ["#MapSet<", to_doc(MapSet.to_list(dict), opts), ">"]
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.new("`,
the document returned by `Inspect.Algebra.to_doc/2`, and the final
string `")"`. Therefore, the MapSet with the numbers 1, 2, and 3
will be printed as:
The `concat` function comes from `Inspect.Algebra` and it
concatenates algebra documents together. In the example above,
it is concatenating the string `"MapSet<"` (all strings are
valid algebra documents that keep their formatting when pretty
printed), the document returned by `Inspect.Algebra.to_doc/2` and the
other string `">"`.
iex> MapSet.new([1, 2, 3], fn x -> x * 2 end)
MapSet.new([2, 4, 6])
Since regular strings are valid entities in an algebra document,
an implementation of inspect may simply return a string,
although that will devoid it of any pretty-printing.
In other words, `MapSet`'s inspect representation returns an expression
that, when evaluated, builds the `MapSet` itself.
### Error handling
## Error handling
In case there is an error while your structure is being inspected,
Elixir will raise an `ArgumentError` error and will automatically fall back
to a raw representation for printing the structure.
You can however access the underlying error by invoking the `Inspect`
implementation directly. For example, to test `Inspect.MapSet` above,
You can however access the underlying error by invoking the Inspect
implementation directly. For example, to test Inspect.MapSet above,
you can invoke it as:
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
Inspect.MapSet.inspect(MapSet.new, %Inspect.Opts{})
"""
# Handle structs in Any
@fallback_to_any true
@doc """
Converts `term` into an algebra document.
This function shouldn't be invoked directly, unless when implementing
a custom `inspect_fun` to be given to `Inspect.Opts`. Everywhere else,
`Inspect.Algebra.to_doc/2` should be preferred as it handles structs
and exceptions.
"""
@spec inspect(t, Inspect.Opts.t()) :: Inspect.Algebra.t()
def inspect(term, opts)
end
defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
def inspect(atom, _opts) do
inspect(atom)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
defp color_key(nil), do: nil
defp color_key(_), do: :atom
def inspect(false), do: "false"
def inspect(true), do: "true"
def inspect(nil), do: "nil"
def inspect(:""), do: ":\"\""
def inspect(atom) do
binary = Atom.to_string(atom)
cond do
valid_ref_identifier?(binary) ->
if only_elixir?(binary) do
binary
else
"Elixir." <> rest = binary
rest
end
valid_atom_identifier?(binary) ->
":" <> binary
atom in [:%{}, :{}, :<<>>, :..., :%] ->
":" <> binary
atom in Macro.binary_ops or atom in Macro.unary_ops ->
":" <> binary
true ->
<<?:, ?", Inspect.BitString.escape(binary, ?")::binary, ?">>
end
end
defp only_elixir?("Elixir." <> rest), do: only_elixir?(rest)
defp only_elixir?("Elixir"), do: true
defp only_elixir?(_), do: false
# Detect if atom is an atom alias (Elixir.Foo.Bar.Baz)
defp valid_ref_identifier?("Elixir" <> rest) do
valid_ref_piece?(rest)
end
defp valid_ref_identifier?(_), do: false
defp valid_ref_piece?(<<?., h, t::binary>>) when h in ?A..?Z do
valid_ref_piece? valid_identifier?(t)
end
defp valid_ref_piece?(<<>>), do: true
defp valid_ref_piece?(_), do: false
# Detect if atom
defp valid_atom_identifier?(<<h, t::binary>>) when h in ?a..?z or h in ?A..?Z or h == ?_ do
valid_atom_piece?(t)
end
defp valid_atom_identifier?(_), do: false
defp valid_atom_piece?(t) do
case valid_identifier?(t) do
<<>> -> true
<<??>> -> true
<<?!>> -> true
<<?@, t::binary>> -> valid_atom_piece?(t)
_ -> false
end
end
defp valid_identifier?(<<h, t::binary>>)
when h in ?a..?z
when h in ?A..?Z
when h in ?0..?9
when h == ?_ do
valid_identifier? t
end
defp valid_identifier?(other), do: other
end
defimpl Inspect, for: BitString do
def inspect(term, opts) when is_binary(term) do
%Inspect.Opts{binaries: bins, base: base, printable_limit: printable_limit} = opts
if base == :decimal and
(bins == :as_strings or (bins == :infer and String.printable?(term, printable_limit))) do
inspected =
case Identifier.escape(term, ?", printable_limit) do
{escaped, ""} -> [?", escaped, ?"]
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color(IO.iodata_to_binary(inspected), :string, opts)
def inspect(term, %Inspect.Opts{binaries: bins, base: base} = opts) when is_binary(term) do
if base == :decimal and (bins == :as_strings or (bins == :infer and String.printable?(term))) do
<<?", escape(term, ?")::binary, ?">>
else
inspect_bitstring(term, opts)
end
@@ -176,15 +153,100 @@ defimpl Inspect, for: BitString do
inspect_bitstring(term, opts)
end
defp inspect_bitstring("", opts) do
color("<<>>", :binary, opts)
## Escaping
@doc false
def escape(other, char) do
escape(other, char, <<>>)
end
defp escape(<<char, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, char>>)
end
defp escape(<<?#, ?{, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?#, ?{>>)
end
defp escape(<<?\a, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?a>>)
end
defp escape(<<?\b, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?b>>)
end
defp escape(<<?\d, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?d>>)
end
defp escape(<<?\e, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?e>>)
end
defp escape(<<?\f, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?f>>)
end
defp escape(<<?\n, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?n>>)
end
defp escape(<<?\r, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?r>>)
end
defp escape(<<?\\, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?\\>>)
end
defp escape(<<?\t, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?t>>)
end
defp escape(<<?\v, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?v>>)
end
defp escape(<<h::utf8, t::binary>>, char, binary) do
head = <<h::utf8>>
if String.printable?(head) do
escape(t, char, append(head, binary))
else
<<byte::8, h::binary>> = head
t = <<h::binary, t::binary>>
escape(t, char, <<binary::binary, escape_char(byte)::binary>>)
end
end
defp escape(<<h, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, escape_char(h)::binary>>)
end
defp escape(<<>>, _char, binary), do: binary
@doc false
# Also used by Regex
def escape_char(0) do
<<?\\, ?0>>
end
def escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
<<?\\, ?x, to_hex(a), to_hex(b)>>
end
def escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
end
def escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c),
to_hex(d), to_hex(e), to_hex(f), ?}>>
end
defp to_hex(c) when c in 0..9, do: ?0+c
defp to_hex(c) when c in 10..15, do: ?A+c-10
defp append(<<h, t::binary>>, binary), do: append(t, <<binary::binary, h>>)
defp append(<<>>, binary), do: binary
## Bitstrings
defp inspect_bitstring("", _opts) do
"<<>>"
end
defp inspect_bitstring(bitstring, opts) do
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
nest surround("<<", each_bit(bitstring, opts.limit, opts), ">>"), 1
end
defp each_bit(_, 0, _) do
@@ -200,82 +262,53 @@ defimpl Inspect, for: BitString do
end
defp each_bit(<<h, t::bitstring>>, counter, opts) do
flex_glue(
concat(Inspect.Integer.inspect(h, opts), ","),
each_bit(t, decrement(counter), opts)
)
glue(concat(Inspect.Integer.inspect(h, opts), ","),
each_bit(t, decrement(counter), opts))
end
defp each_bit(bitstring, _counter, opts) do
size = bit_size(bitstring)
<<h::size(size)>> = bitstring
concat(Inspect.Integer.inspect(h, opts), "::size(" <> Integer.to_string(size) <> ")")
Inspect.Integer.inspect(h, opts) <> "::size(" <> Integer.to_string(size) <> ")"
end
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
defp decrement(counter), do: counter - 1
end
defimpl Inspect, for: List do
def inspect([], opts) do
color("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
def inspect(term, opts) do
%Inspect.Opts{
charlists: lists,
char_lists: lists_deprecated,
printable_limit: printable_limit
} = opts
def inspect([], _opts), do: "[]"
# TODO: Deprecate :char_lists and :as_char_lists keys in v1.5
def inspect(term, %Inspect.Opts{charlists: lists, char_lists: lists_deprecated} = opts) do
lists =
if lists == :infer and lists_deprecated != :infer do
case lists_deprecated do
:as_char_lists ->
IO.warn(
"the :char_lists inspect option and its :as_char_lists " <>
"value are deprecated, use the :charlists option and its " <>
":as_charlists value instead"
)
:as_charlists
_ ->
IO.warn("the :char_lists inspect option is deprecated, use :charlists instead")
lists_deprecated
end
else
lists
end
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?', printable_limit) do
{escaped, ""} -> [?', escaped, ?']
{escaped, _} -> [?', escaped, ?', " ++ ..."]
end
color(IO.iodata_to_binary(inspected), :charlist, opts)
lists == :as_charlists or (lists == :infer and printable?(term)) ->
<<?', Inspect.BitString.escape(IO.chardata_to_string(term), ?')::binary, ?'>>
keyword?(term) ->
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
surround_many("[", term, "]", opts, &keyword/2)
true ->
container_doc(open, term, close, opts, &to_doc/2, separator: sep)
surround_many("[", term, "]", opts, &to_doc/2)
end
end
@doc false
def keyword({key, value}, opts) do
key = color(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
concat(
key_to_binary(key) <> ": ",
to_doc(value, opts)
)
end
@doc false
@@ -286,171 +319,175 @@ defimpl Inspect, for: List do
end
end
def keyword?([]), do: true
def keyword?([]), do: true
def keyword?(_other), do: false
@doc false
def printable?([c | cs]) when c in 32..126, do: printable?(cs)
def printable?([?\n | cs]), do: printable?(cs)
def printable?([?\r | cs]), do: printable?(cs)
def printable?([?\t | cs]), do: printable?(cs)
def printable?([?\v | cs]), do: printable?(cs)
def printable?([?\b | cs]), do: printable?(cs)
def printable?([?\f | cs]), do: printable?(cs)
def printable?([?\e | cs]), do: printable?(cs)
def printable?([?\a | cs]), do: printable?(cs)
def printable?([]), do: true
def printable?(_), do: false
## Private
defp key_to_binary(key) do
case Inspect.Atom.inspect(key) do
":" <> right -> right
other -> other
end
end
end
defimpl Inspect, for: Tuple do
def inspect({}, _opts), do: "{}"
def inspect(tuple, opts) do
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
surround_many("{", Tuple.to_list(tuple), "}", opts, &to_doc/2)
end
end
defimpl Inspect, for: Map do
def inspect(map, opts) do
list =
if Keyword.get(opts.custom_options, :sort_maps) do
map |> Map.to_list() |> :lists.sort()
else
Map.to_list(map)
end
fun =
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color(" => ", :map, opts)
&to_assoc(&1, &2, sep)
end
map_container_doc(list, "", opts, fun)
nest inspect(map, "", opts), 1
end
def inspect(map, name, infos, opts) do
fun = fn %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) end
map_container_doc(infos, name, opts, fun)
def inspect(map, name, opts) do
map = :maps.to_list(map)
surround_many("%" <> name <> "{", map, "}", opts, traverse_fun(map))
end
defp to_assoc({key, value}, opts, sep) do
concat(concat(to_doc(key, opts), sep), to_doc(value, opts))
defp traverse_fun(list) do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
&to_map/2
end
end
defp map_container_doc(list, name, opts, fun) do
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
defp to_map({key, value}, opts) do
concat(
concat(to_doc(key, opts), " => "),
to_doc(value, opts)
)
end
end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color(inspected, :number, opts)
def inspect(term, %Inspect.Opts{base: base}) do
Integer.to_string(term, base_to_value(base))
|> prepend_prefix(base)
end
defp base_to_value(base) do
case base do
:binary -> 2
:binary -> 2
:decimal -> 10
:octal -> 8
:hex -> 16
:octal -> 8
:hex -> 16
end
end
defp prepend_prefix(value, :decimal), do: value
defp prepend_prefix(<<?-, value::binary>>, base) do
"-" <> prepend_prefix(value, base)
end
defp prepend_prefix(value, base) do
prefix =
case base do
:binary -> "0b"
:octal -> "0o"
:hex -> "0x"
end
prefix = case base do
:binary -> "0b"
:octal -> "0o"
:hex -> "0x"
end
prefix <> value
end
end
defimpl Inspect, for: Float do
def inspect(float, opts) do
abs = abs(float)
formatted =
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
[Integer.to_string(trunc(float)), ?., ?0]
else
:io_lib_format.fwrite_g(float)
end
color(IO.iodata_to_binary(formatted), :number, opts)
def inspect(term, _opts) do
IO.iodata_to_binary(:io_lib_format.fwrite_g(term))
end
end
defimpl Inspect, for: Regex do
def inspect(regex = %{opts: regex_opts}, opts) when is_list(regex_opts) do
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
def inspect(regex, _opts) do
delim = ?/
concat ["~r",
<<delim, escape(regex.source, delim)::binary, delim>>,
regex.opts]
end
def inspect(regex, opts) do
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
defp escape(bin, term),
do: escape(bin, <<>>, term)
source = IO.iodata_to_binary(['~r/', escaped, ?/, regex.opts])
color(source, :regex, opts)
defp escape(<<?\\, term>> <> rest, buf, term),
do: escape(rest, buf <> <<?\\, term>>, term)
defp escape(<<term>> <> rest, buf, term),
do: escape(rest, buf <> <<?\\, term>>, term)
# the list of characters is from "String.printable?" impl
# minus characters treated specially by regex: \s, \d, \b, \e
defp escape(<<?\n>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?n>>, term)
defp escape(<<?\r>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?r>>, term)
defp escape(<<?\t>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?t>>, term)
defp escape(<<?\v>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?v>>, term)
defp escape(<<?\f>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?f>>, term)
defp escape(<<?\a>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?a>>, term)
defp escape(<<c::utf8>> <> rest, buf, term) do
charstr = <<c::utf8>>
if String.printable?(charstr) and not c in [?\d, ?\b, ?\e] do
escape(rest, buf <> charstr, term)
else
escape(rest, buf <> Inspect.BitString.escape_char(c), term)
end
end
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
defp normalize(<<char, rest::binary>>, acc), do: normalize(rest, <<acc::binary, char>>)
defp normalize(<<>>, acc), do: acc
defp escape(<<c>> <> rest, buf, term),
do: escape(rest, <<buf::binary, Inspect.BitString.escape_char(c)>>, term)
defp escape_map(?\a), do: '\\a'
defp escape_map(?\f), do: '\\f'
defp escape_map(?\n), do: '\\n'
defp escape_map(?\r), do: '\\r'
defp escape_map(?\t), do: '\\t'
defp escape_map(?\v), do: '\\v'
defp escape_map(_), do: false
defp escape(<<>>, buf, _), do: buf
end
defimpl Inspect, for: Function do
def inspect(function, _opts) do
fun_info = Function.info(function)
fun_info = :erlang.fun_info(function)
mod = fun_info[:module]
name = fun_info[:name]
cond do
not is_atom(mod) ->
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]}>"
fun_info[:type] == :external and fun_info[:env] == [] ->
inspected_as_atom = Macro.inspect_atom(:literal, mod)
inspected_as_function = Macro.inspect_atom(:remote_call, name)
"&#{inspected_as_atom}.#{inspected_as_function}/#{fun_info[:arity]}"
match?('elixir_compiler_' ++ _, Atom.to_charlist(mod)) ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
if fun_info[:type] == :external and fun_info[:env] == [] do
"&#{Inspect.Atom.inspect(mod)}.#{fun_info[:name]}/#{fun_info[:arity]}"
else
case Atom.to_charlist(mod) do
'elixir_compiler_' ++ _ ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
default_inspect(mod, fun_info)
end
_ ->
default_inspect(mod, fun_info)
end
true ->
default_inspect(mod, fun_info)
end
end
end
defp default_inspect(mod, fun_info) do
inspected_as_atom = Macro.inspect_atom(:literal, mod)
extracted_name = extract_name(fun_info[:name])
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in #{inspected_as_atom}#{extracted_name}>"
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in " <>
"#{Inspect.Atom.inspect(mod)}#{extract_name(fun_info[:name])}>"
end
defp extract_name([]) do
@@ -458,47 +495,16 @@ defimpl Inspect, for: Function do
end
defp extract_name(name) do
case Identifier.extract_anonymous_fun_parent(name) do
{name, arity} ->
"." <> Macro.inspect_atom(:remote_call, name) <> "/" <> arity
:error ->
"." <> Macro.inspect_atom(:remote_call, name)
name = Atom.to_string(name)
case :binary.split(name, "-", [:global]) do
["", name | _] -> "." <> name
_ -> "." <> name
end
end
defp uniq(fun_info) do
Integer.to_string(fun_info[:new_index]) <> "." <> Integer.to_string(fun_info[:uniq])
end
end
defimpl Inspect, for: Inspect.Error do
@impl true
def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do
message = Exception.message(inspect_error)
format_output(message, stacktrace)
end
defp format_output(message, [_ | _] = stacktrace) do
stacktrace = Exception.format_stacktrace(stacktrace)
"""
#Inspect.Error<
#{Inspect.Error.pad(message, 2)}
Stacktrace:
#{stacktrace}
>\
"""
end
defp format_output(message, []) do
"""
#Inspect.Error<
#{Inspect.Error.pad(message, 2)}
>\
"""
Integer.to_string(fun_info[:new_index]) <> "." <>
Integer.to_string(fun_info[:uniq])
end
end
@@ -510,7 +516,7 @@ end
defimpl Inspect, for: Port do
def inspect(port, _opts) do
IO.iodata_to_binary(:erlang.port_to_list(port))
IO.iodata_to_binary :erlang.port_to_list(port)
end
end
@@ -522,123 +528,19 @@ defimpl Inspect, for: Reference do
end
defimpl Inspect, for: Any do
defmacro __deriving__(module, struct, options) do
fields = Map.keys(struct) -- [:__exception__, :__struct__]
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == only and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
optional? =
if optional == [] do
false
else
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
quote do
case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
var!(field) in unquote(filtered_fields) and not unquote(optional?),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
def inspect(%module{} = struct, opts) do
def inspect(%{__struct__: struct} = map, opts) do
try do
{module.__struct__(), module.__info__(:struct)}
struct.__struct__
rescue
_ -> Inspect.Map.inspect(struct, opts)
_ -> Inspect.Map.inspect(map, opts)
else
{dunder, fields} ->
if Map.keys(dunder) == Map.keys(struct) do
infos =
for %{field: field} = info <- fields,
field not in [:__struct__, :__exception__],
do: info
Inspect.Map.inspect(struct, Macro.inspect_atom(:literal, module), infos, opts)
dunder ->
if :maps.keys(dunder) == :maps.keys(map) do
pruned = :maps.remove(:__exception__, :maps.remove(:__struct__, map))
Inspect.Map.inspect(pruned, Inspect.Atom.inspect(struct, opts), opts)
else
Inspect.Map.inspect(struct, opts)
Inspect.Map.inspect(map, opts)
end
end
end
def inspect(map, name, infos, opts) do
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
fun = fn
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
:..., _opts -> "..."
end
container_doc(open, infos ++ [:...], close, opts, fun, separator: sep, break: :strict)
end
end
require Protocol
Protocol.derive(
Inspect,
Macro.Env,
only: [
:module,
:file,
:line,
:function,
:context,
:aliases,
:requires,
:functions,
:macros,
:macro_aliases,
:context_modules,
:lexical_tracker
]
)
File diff suppressed because it is too large Load Diff
+119 -271
View File
@@ -1,15 +1,6 @@
defmodule Integer do
@moduledoc """
Functions for working with integers.
Some functions that work on integers are found in `Kernel`:
* `Kernel.abs/1`
* `Kernel.div/2`
* `Kernel.max/2`
* `Kernel.min/2`
* `Kernel.rem/2`
"""
import Bitwise
@@ -37,7 +28,9 @@ defmodule Integer do
false
"""
defguard is_odd(integer) when is_integer(integer) and (integer &&& 1) == 1
defmacro is_odd(integer) do
quote do: (unquote(integer) &&& 1) == 1
end
@doc """
Determines if an `integer` is even.
@@ -62,146 +55,44 @@ defmodule Integer do
true
"""
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
@doc """
Computes `base` raised to power of `exponent`.
Both `base` and `exponent` must be integers.
The exponent must be zero or positive.
See `Float.pow/2` for exponentiation of negative
exponents as well as floats.
## Examples
iex> Integer.pow(2, 0)
1
iex> Integer.pow(2, 1)
2
iex> Integer.pow(2, 10)
1024
iex> Integer.pow(2, 11)
2048
iex> Integer.pow(2, 64)
0x10000000000000000
iex> Integer.pow(3, 4)
81
iex> Integer.pow(4, 3)
64
iex> Integer.pow(-2, 3)
-8
iex> Integer.pow(-2, 4)
16
iex> Integer.pow(2, -2)
** (ArithmeticError) bad argument in arithmetic expression
"""
@doc since: "1.12.0"
@spec pow(integer, non_neg_integer) :: integer
def pow(base, exponent) when is_integer(base) and is_integer(exponent) do
if exponent < 0, do: :erlang.error(:badarith, [base, exponent])
base ** exponent
defmacro is_even(integer) do
quote do: (unquote(integer) &&& 1) == 0
end
@doc """
Computes the modulo remainder of an integer division.
This function performs a [floored division](`floor_div/2`), which means that
the result will always have the sign of the `divisor`.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
## Examples
iex> Integer.mod(5, 2)
1
iex> Integer.mod(6, -4)
-2
"""
@doc since: "1.4.0"
@spec mod(integer, neg_integer | pos_integer) :: integer
def mod(dividend, divisor) do
remainder = rem(dividend, divisor)
if remainder * divisor < 0 do
remainder + divisor
else
remainder
end
end
@doc """
Performs a floored integer division.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
This function performs a *floored* integer division, which means that
the result will always be rounded towards negative infinity.
If you want to perform truncated integer division (rounding towards zero),
use `Kernel.div/2` instead.
## Examples
iex> Integer.floor_div(5, 2)
2
iex> Integer.floor_div(6, -4)
-2
iex> Integer.floor_div(-99, 2)
-50
"""
@doc since: "1.4.0"
@spec floor_div(integer, neg_integer | pos_integer) :: integer
def floor_div(dividend, divisor) do
if dividend * divisor < 0 and rem(dividend, divisor) != 0 do
div(dividend, divisor) - 1
else
div(dividend, divisor)
end
end
@doc """
Returns the ordered digits for the given `integer`.
Returns the ordered digits for the given non-negative `integer`.
An optional `base` value may be provided representing the radix for the returned
digits. This one must be an integer >= 2.
digits. This one can be an integer >= 2.
## Examples
iex> Integer.digits(123)
[1, 2, 3]
iex> Integer.digits(101)
[1, 0, 1]
iex> Integer.digits(170, 2)
[1, 0, 1, 0, 1, 0, 1, 0]
iex> Integer.digits(-170, 2)
[-1, 0, -1, 0, -1, 0, -1, 0]
"""
@spec digits(integer, pos_integer) :: [integer, ...]
@spec digits(non_neg_integer, pos_integer) :: [non_neg_integer, ...]
def digits(integer, base \\ 10)
when is_integer(integer) and is_integer(base) and base >= 2 do
when is_integer(integer) and integer >= 0 and is_integer(base) and base >= 2 do
do_digits(integer, base, [])
end
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
defp do_digits(0, _base, []), do: [0]
defp do_digits(1, _base, []), do: [1]
defp do_digits(base, base, []), do: [1, 0]
defp do_digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc) do
do_digits div(integer, base), base, [rem(integer, base) | acc]
end
@doc """
Returns the integer represented by the ordered `digits`.
An optional `base` value may be provided representing the radix for the `digits`.
Base has to be an integer greater than or equal to `2`.
This one can be an integer >= 2.
## Examples
@@ -215,18 +106,19 @@ defmodule Integer do
0
"""
@spec undigits([integer], pos_integer) :: integer
@spec undigits([integer], integer) :: integer
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
do_undigits(digits, base, 0)
end
defp do_undigits([], _base, []), do: 0
defp do_undigits([0], _base, []), do: 0
defp do_undigits([1], _base, []), do: 1
defp do_undigits([1, 0], base, []), do: base
defp do_undigits([], _base, acc), do: acc
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
defp do_undigits([digit | tail], base, acc) do
do_undigits(tail, base, acc * base + digit)
end
@doc """
Parses a text representation of an integer.
@@ -239,7 +131,7 @@ defmodule Integer do
Raises an error if `base` is less than 2 or more than 36.
If you want to convert a string-formatted integer directly to an integer,
If you want to convert a string-formatted integer directly to a integer,
`String.to_integer/1` or `String.to_integer/2` can be used instead.
## Examples
@@ -269,55 +161,78 @@ defmodule Integer do
** (ArgumentError) invalid base 38
"""
@spec parse(binary, 2..36) :: {integer, remainder_of_binary :: binary} | :error
@spec parse(binary, 2..36) :: {integer, binary} | :error | no_return
def parse(binary, base \\ 10)
def parse(_binary, base) when base not in 2..36 do
raise ArgumentError, "invalid base #{inspect(base)}"
def parse("", base) when base in 2..36,
do: :error
def parse(binary, base) when is_binary(binary) and base in 2..36 do
parse_in_base(binary, base)
end
def parse(binary, base) when is_binary(binary) do
case count_digits(binary, base) do
0 ->
:error
raise ArgumentError, "invalid base #{base}"
end
count ->
{digits, rem} = :erlang.split_binary(binary, count)
{:erlang.binary_to_integer(digits, base), rem}
defp parse_in_base("-" <> bin, base) do
case do_parse(bin, base) do
{number, remainder} -> {-number, remainder}
:error -> :error
end
end
defp count_digits(<<sign, rest::bits>>, base) when sign in '+-' do
case count_digits_nosign(rest, base, 1) do
1 -> 0
count -> count
defp parse_in_base("+" <> bin, base) do
do_parse(bin, base)
end
defp parse_in_base(binary, base) when is_binary(binary) do
do_parse(binary, base)
end
defp do_parse(<<char, rest::binary>>, base) do
if valid_digit_in_base?(char, base) do
do_parse(rest, base, parse_digit(char, base))
else
:error
end
end
defp count_digits(<<rest::bits>>, base) do
count_digits_nosign(rest, base, 0)
defp do_parse(_, _) do
:error
end
digits = [{?0..?9, -?0}, {?A..?Z, 10 - ?A}, {?a..?z, 10 - ?a}]
for {chars, diff} <- digits,
char <- chars do
digit = char + diff
defp count_digits_nosign(<<unquote(char), rest::bits>>, base, count)
when base > unquote(digit) do
count_digits_nosign(rest, base, count + 1)
defp do_parse(<<char, rest::binary>> = bin, base, acc) do
if valid_digit_in_base?(char, base) do
do_parse(rest, base, base * acc + parse_digit(char, base))
else
{acc, bin}
end
end
defp count_digits_nosign(<<_::bits>>, _, count), do: count
defp do_parse(bitstring, _, acc) do
{acc, bitstring}
end
defp parse_digit(char, _) do
cond do
char in ?0..?9 -> char - ?0
char in ?A..?Z -> char - ?A + 10
true -> char - ?a + 10
end
end
defp valid_digit_in_base?(char, base) do
if base <= 10 do
char in ?0..(?0 + base - 1)
else
char in ?0..?9 or char in ?A..(?A + base - 11) or char in ?a..(?a + base - 11)
end
end
@doc """
Returns a binary which corresponds to the text representation
of `integer` in the given `base`.
`base` can be an integer between 2 and 36. If no `base` is given,
it defaults to `10`.
of `integer`.
Inlined by the compiler.
@@ -335,27 +250,39 @@ defmodule Integer do
iex> Integer.to_string(0123)
"123"
"""
@spec to_string(integer) :: String.t
def to_string(integer) do
:erlang.integer_to_binary(integer)
end
@doc """
Returns a binary which corresponds to the text representation
of `integer` in the given `base`.
`base` can be an integer between 2 and 36.
Inlined by the compiler.
## Examples
iex> Integer.to_string(100, 16)
"64"
iex> Integer.to_string(-100, 16)
"-64"
iex> Integer.to_string(882_681_651, 36)
iex> Integer.to_string(882681651, 36)
"ELIXIR"
"""
@spec to_string(integer, 2..36) :: String.t()
def to_string(integer, base \\ 10) do
@spec to_string(integer, 2..36) :: String.t
def to_string(integer, base) do
:erlang.integer_to_binary(integer, base)
end
@doc """
Returns a charlist which corresponds to the text representation
of `integer` in the given `base`.
`base` can be an integer between 2 and 36. If no `base` is given,
it defaults to `10`.
Returns a charlist which corresponds to the text representation of the given `integer`.
Inlined by the compiler.
@@ -373,117 +300,38 @@ defmodule Integer do
iex> Integer.to_charlist(0123)
'123'
"""
@spec to_charlist(integer) :: charlist
def to_charlist(integer) do
:erlang.integer_to_list(integer)
end
@doc """
Returns a charlist which corresponds to the text representation of `integer` in the given `base`.
`base` can be an integer between 2 and 36.
Inlined by the compiler.
## Examples
iex> Integer.to_charlist(100, 16)
'64'
iex> Integer.to_charlist(-100, 16)
'-64'
iex> Integer.to_charlist(882_681_651, 36)
iex> Integer.to_charlist(882681651, 36)
'ELIXIR'
"""
@spec to_charlist(integer, 2..36) :: charlist
def to_charlist(integer, base \\ 10) do
def to_charlist(integer, base) do
:erlang.integer_to_list(integer, base)
end
@doc """
Returns the greatest common divisor of the two given integers.
The greatest common divisor (GCD) of `integer1` and `integer2` is the largest positive
integer that divides both `integer1` and `integer2` without leaving a remainder.
By convention, `gcd(0, 0)` returns `0`.
## Examples
iex> Integer.gcd(2, 3)
1
iex> Integer.gcd(8, 12)
4
iex> Integer.gcd(8, -12)
4
iex> Integer.gcd(10, 0)
10
iex> Integer.gcd(7, 7)
7
iex> Integer.gcd(0, 0)
0
"""
@doc since: "1.5.0"
@spec gcd(integer, integer) :: non_neg_integer
def gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
gcd_positive(abs(integer1), abs(integer2))
end
defp gcd_positive(0, integer2), do: integer2
defp gcd_positive(integer1, 0), do: integer1
defp gcd_positive(integer1, integer2), do: gcd_positive(integer2, rem(integer1, integer2))
@doc """
Returns the extended greatest common divisor of the two given integers.
This function uses the extended Euclidean algorithm to return a three-element tuple with the `gcd`
and the coefficients `m` and `n` of Bézout's identity such that:
gcd(a, b) = m*a + n*b
By convention, `extended_gcd(0, 0)` returns `{0, 0, 0}`.
## Examples
iex> Integer.extended_gcd(240, 46)
{2, -9, 47}
iex> Integer.extended_gcd(46, 240)
{2, 47, -9}
iex> Integer.extended_gcd(-46, 240)
{2, -47, -9}
iex> Integer.extended_gcd(-46, -240)
{2, -47, 9}
iex> Integer.extended_gcd(14, 21)
{7, -1, 1}
iex> Integer.extended_gcd(10, 0)
{10, 1, 0}
iex> Integer.extended_gcd(0, 10)
{10, 0, 1}
iex> Integer.extended_gcd(0, 0)
{0, 0, 0}
"""
@doc since: "1.12.0"
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
def extended_gcd(0, 0), do: {0, 0, 0}
def extended_gcd(0, b), do: {b, 0, 1}
def extended_gcd(a, 0), do: {a, 1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0)
end
defp extended_gcd(r1, r0, s1, s0, t1, t0) do
div = div(r0, r1)
case r0 - div * r1 do
0 when r1 > 0 -> {r1, s1, t1}
0 when r1 < 0 -> {-r1, -s1, -t1}
r2 -> extended_gcd(r2, r1, s0 - div * s1, s1, t0 - div * t1, t1)
end
end
# TODO: Deprecate by v1.5
@doc false
@deprecated "Use Integer.to_charlist/1 instead"
@spec to_char_list(integer) :: charlist
def to_char_list(integer), do: Integer.to_charlist(integer)
@doc false
@deprecated "Use Integer.to_charlist/2 instead"
def to_char_list(integer, base), do: Integer.to_charlist(integer, base)
end
+125 -392
View File
@@ -1,20 +1,23 @@
defmodule IO do
@moduledoc ~S"""
@moduledoc """
Functions handling input/output (IO).
Many functions in this module expect an IO device as an argument.
An IO device must be a PID or an atom representing a process.
An IO device must be a pid or an atom representing a process.
For convenience, Elixir provides `:stdio` and `:stderr` as
shortcuts to Erlang's `:standard_io` and `:standard_error`.
The majority of the functions expect chardata. In case another type is given,
functions will convert those types to string via the `String.Chars` protocol
(as shown in typespecs). For more information on chardata, see the
"IO data" section below.
The majority of the functions expect chardata, i.e. strings or
lists of characters and strings. In case another type is given,
functions will convert to string via the `String.Chars` protocol
(as shown in typespecs).
The functions starting with `bin` expect iodata as an argument,
i.e. binaries or lists of bytes and binaries.
## IO devices
An IO device may be an atom or a PID. In case it is an atom,
An IO device may be an atom or a pid. In case it is an atom,
the atom must be the name of a registered process. In addition,
Elixir provides two shortcuts:
@@ -24,116 +27,35 @@ defmodule IO do
* `:stderr` - a shortcut for the named process `:standard_error`
provided in Erlang
IO devices maintain their position, which means subsequent calls to any
reading or writing functions will start from the place where the device
was last accessed. The position of files can be changed using the
IO devices maintain their position, that means subsequent calls to any
reading or writing functions will start from the place when the device
was last accessed. Position of files can be changed using the
`:file.position/2` function.
## IO data
IO data is a data type that can be used as a more efficient alternative to binaries
in certain situations.
A term of type **IO data** is a binary or a list containing bytes (integers within the `0..255` range)
or nested IO data. The type is recursive. Let's see an example of one of
the possible IO data representing the binary `"hello"`:
[?h, "el", ["l", [?o]]]
The built-in `t:iodata/0` type is defined in terms of `t:iolist/0`. An IO list is
the same as IO data but it doesn't allow for a binary at the top level (but binaries
are still allowed in the list itself).
### Use cases for IO data
IO data exists because often you need to do many append operations
on smaller chunks of binaries in order to create a bigger binary. However, in
Erlang and Elixir concatenating binaries will copy the concatenated binaries
into a new binary.
def email(username, domain) do
username <> "@" <> domain
end
In this function, creating the email address will copy the `username` and `domain`
binaries. Now imagine you want to use the resulting email inside another binary:
def welcome_message(name, username, domain) do
"Welcome #{name}, your email is: #{email(username, domain)}"
end
IO.puts(welcome_message("Meg", "meg", "example.com"))
#=> "Welcome Meg, your email is: meg@example.com"
Every time you concatenate binaries or use interpolation (`#{}`) you are making
copies of those binaries. However, in many cases you don't need the complete
binary while you create it, but only at the end to print it out or send it
somewhere. In such cases, you can construct the binary by creating IO data:
def email(username, domain) do
[username, ?@, domain]
end
def welcome_message(name, username, domain) do
["Welcome ", name, ", your email is: ", email(username, domain)]
end
IO.puts(welcome_message("Meg", "meg", "example.com"))
#=> "Welcome Meg, your email is: meg@example.com"
Building IO data is cheaper than concatenating binaries. Concatenating multiple
pieces of IO data just means putting them together inside a list since IO data
can be arbitrarily nested, and that's a cheap and efficient operation. Most of
the IO-based APIs, such as `:gen_tcp` and `IO`, receive IO data and write it
to the socket directly without converting it to binary.
One drawback of IO data is that you can't do things like pattern match on the
first part of a piece of IO data like you can with a binary, because you usually
don't know the shape of the IO data. In those cases, you may need to convert it
to a binary by calling `iodata_to_binary/1`, which is reasonably efficient
since it's implemented natively in C. Other functionality, like computing the
length of IO data, can be computed directly on the iodata by calling `iodata_length/1`.
### Chardata
Erlang and Elixir also have the idea of `t:chardata/0`. Chardata is very
similar to IO data: the only difference is that integers in IO data represent
bytes while integers in chardata represent Unicode code points. Bytes
(`t:byte/0`) are integers within the `0..255` range, while Unicode code points
(`t:char/0`) are integers within the `0..0x10FFFF` range. The `IO` module provides
the `chardata_to_string/1` function for chardata as the "counter-part" of the
`iodata_to_binary/1` function for IO data.
If you try to use `iodata_to_binary/1` on chardata, it will result in an
argument error. For example, let's try to put a code point that is not
representable with one byte, like `?π`, inside IO data:
IO.iodata_to_binary(["The symbol for pi is: ", ?π])
#=> ** (ArgumentError) argument error
If we use chardata instead, it will work as expected:
iex> IO.chardata_to_string(["The symbol for pi is: ", ?π])
"The symbol for pi is: π"
"""
@type device :: atom | pid
@type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type chardata() :: :unicode.chardata()
defguardp is_device(term) when is_atom(term) or is_pid(term)
defguardp is_iodata(data) when is_list(data) or is_binary(data)
import :erlang, only: [group_leader: 0]
defmacrop is_iodata(data) do
quote do
is_list(unquote(data)) or is_binary(unquote(data))
end
end
@doc """
Reads from the IO `device`.
The `device` is iterated by the given number of characters, line by line if
`:line` is given, or until `:eof`.
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
Alternatively, if `:all` is given, then whole `device` is returned.
It returns:
* `data` - the output characters
* `data` - the input characters
* `:eof` - end of file was encountered
@@ -141,24 +63,14 @@ defmodule IO do
for instance, `{:error, :estale}` if reading from an
NFS volume
If `:all` is given, `:eof` is never returned, but an
empty string in case the device has reached EOF.
"""
@spec read(device, :eof | :line | non_neg_integer) :: chardata | nodata
def read(device \\ :stdio, line_or_chars)
@spec read(device, :all | :line | non_neg_integer) :: chardata | nodata
def read(device \\ group_leader(), line_or_chars)
# TODO: Deprecate me on v1.17
def read(device, :all) do
with :eof <- read(device, :eof) do
with [_ | _] = opts <- :io.getopts(device),
false <- Keyword.get(opts, :binary, true) do
''
else
_ -> ""
end
end
end
def read(device, :eof) do
getn(device, '', :eof)
do_read_all(map_dev(device), "")
end
def read(device, :line) do
@@ -169,24 +81,24 @@ defmodule IO do
:io.get_chars(map_dev(device), '', count)
end
defp do_read_all(mapped_dev, acc) do
case :io.get_line(mapped_dev, "") do
line when is_binary(line) -> do_read_all(mapped_dev, acc <> line)
:eof -> acc
other -> other
end
end
@doc """
Reads from the IO `device`. The operation is Unicode unsafe.
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes.
* if `line_or_chars` is `:line`, the device is iterated line by line.
* if `line_or_chars` is `:eof`, the device is iterated until `:eof`. `line_or_chars`
can only be `:eof` since Elixir 1.13.0. `:eof` replaces the deprecated `:all`,
with the difference that `:all` returns `""` on end of file, while `:eof` returns
`:eof` itself.
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
Alternatively, if `:all` is given, then whole `device` is returned.
It returns:
* `data` - the output bytes
* `data` - the input characters
* `:eof` - end of file was encountered
@@ -194,19 +106,17 @@ defmodule IO do
for instance, `{:error, :estale}` if reading from an
NFS volume
If `:all` is given, `:eof` is never returned, but an
empty string in case the device has reached EOF.
Note: do not use this function on IO devices in Unicode mode
as it will return the wrong result.
"""
@spec binread(device, :eof | :line | non_neg_integer) :: iodata | nodata
def binread(device \\ :stdio, line_or_chars)
@spec binread(device, :all | :line | non_neg_integer) :: iodata | nodata
def binread(device \\ group_leader(), line_or_chars)
# TODO: Deprecate me on v1.17
def binread(device, :all) do
with :eof <- binread(device, :eof), do: ""
end
def binread(device, :eof) do
binread_eof(map_dev(device), "")
do_binread_all(map_dev(device), "")
end
def binread(device, :line) do
@@ -224,157 +134,82 @@ defmodule IO do
end
@read_all_size 4096
defp binread_eof(mapped_dev, acc) do
defp do_binread_all(mapped_dev, acc) do
case :file.read(mapped_dev, @read_all_size) do
{:ok, data} -> binread_eof(mapped_dev, acc <> data)
:eof -> if acc == "", do: :eof, else: acc
{:ok, data} -> do_binread_all(mapped_dev, acc <> data)
:eof -> acc
other -> other
end
end
@doc """
Writes `chardata` to the given `device`.
Writes `item` to the given `device`.
By default, the `device` is the standard output.
By default the `device` is the standard output.
It returns `:ok` if it succeeds.
## Examples
IO.write("sample")
IO.write "sample"
#=> sample
IO.write(:stderr, "error")
IO.write :stderr, "error"
#=> error
"""
@spec write(device, chardata | String.Chars.t()) :: :ok
def write(device \\ :stdio, chardata) do
:io.put_chars(map_dev(device), to_chardata(chardata))
@spec write(device, chardata | String.Chars.t) :: :ok
def write(device \\ group_leader(), item) do
:io.put_chars map_dev(device), to_chardata(item)
end
@doc """
Writes `iodata` to the given `device`.
Writes `item` as a binary to the given `device`.
No Unicode conversion happens.
The operation is Unicode unsafe.
This operation is meant to be used with "raw" devices
that are started without an encoding. The given `iodata`
is written as is to the device, without conversion. For
more information on IO data, see the "IO data" section in
the module documentation.
Check `write/2` for more information.
Use `write/2` for devices with encoding.
Important: do **not** use this function on IO devices in
Unicode mode as it will write the wrong data. In particular,
the standard IO device is set to Unicode by default, so writing
to stdio with this function will likely result in the wrong data
being sent down the wire.
Note: do not use this function on IO devices in Unicode mode
as it will return the wrong result.
"""
@spec binwrite(device, iodata) :: :ok | {:error, term}
def binwrite(device \\ :stdio, iodata) when is_iodata(iodata) do
:file.write(map_dev(device), iodata)
def binwrite(device \\ group_leader(), item) when is_iodata(item) do
:file.write map_dev(device), item
end
@doc """
Writes `item` to the given `device`, similar to `write/2`,
but adds a newline at the end.
By default, the `device` is the standard output. It returns `:ok`
if it succeeds.
## Examples
IO.puts("Hello World!")
#=> Hello World!
IO.puts(:stderr, "error")
#=> error
"""
@spec puts(device, chardata | String.Chars.t()) :: :ok
def puts(device \\ :stdio, item) when is_device(device) do
:io.put_chars(map_dev(device), [to_chardata(item), ?\n])
@spec puts(device, chardata | String.Chars.t) :: :ok
def puts(device \\ group_leader(), item) do
:io.put_chars map_dev(device), [to_chardata(item), ?\n]
end
@doc """
Writes a `message` to stderr, along with the given `stacktrace_info`.
The `stacktrace_info` must be one of:
* a `__STACKTRACE__`, where all entries in the stacktrace will be
included in the error message
* a `Macro.Env` structure (since v1.14.0), where a single stacktrace
entry from the compilation environment will be used
* a keyword list with at least the `:file` option representing
a single stacktrace entry (since v1.14.0). The `:line`, `:module`,
`:function` options are also supported
Writes a `message` to stderr, along with the given `stacktrace`.
This function also notifies the compiler a warning was printed
(in case --warnings-as-errors was enabled). It returns `:ok`
if it succeeds.
An empty list can be passed to avoid stacktrace printing.
## Examples
stacktrace = [{MyApp, :main, 1, [file: 'my_app.ex', line: 4]}]
IO.warn("variable bar is unused", stacktrace)
IO.warn "variable bar is unused", stacktrace
#=> warning: variable bar is unused
#=> my_app.ex:4: MyApp.main/1
"""
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
:ok
def warn(message, stacktrace_info)
@spec warn(chardata | String.Chars.t, Exception.stacktrace) :: :ok
def warn(message, []) do
message = [to_chardata(message), ?\n]
:elixir_errors.log_and_print_warning(0, nil, message, message)
:elixir_errors.warn([to_chardata(message), ?\n])
end
def warn(message, %Macro.Env{} = env) do
warn(message, Macro.Env.stacktrace(env))
end
def warn(message, [{_, _} | _] = keyword) do
if file = keyword[:file] do
warn(
message,
%{
__ENV__
| module: keyword[:module],
function: keyword[:function],
line: keyword[:line],
file: file
}
)
else
warn(message, [])
end
end
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
message = to_chardata(message)
formatted_trace = Enum.map_join(stacktrace, "\n ", &Exception.format_stacktrace_entry(&1))
line = opts[:line]
file = opts[:file]
:elixir_errors.log_and_print_warning(
line || 0,
file && List.to_string(file),
message,
[message, ?\n, " ", formatted_trace, ?\n]
)
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
def warn(message, stacktrace) when is_list(stacktrace) do
formatted = Enum.map_join(stacktrace, "\n ", &Exception.format_stacktrace_entry(&1))
:elixir_errors.warn([to_chardata(message), ?\n, " ", formatted, ?\n])
end
@doc """
@@ -382,19 +217,14 @@ defmodule IO do
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")
IO.warn "variable bar is unused"
#=> warning: variable bar is unused
#=> (iex) evaluator.ex:108: IEx.Evaluator.eval/4
"""
@spec warn(chardata | String.Chars.t()) :: :ok
@spec warn(chardata | String.Chars.t) :: :ok
def warn(message) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
warn(message, Enum.drop(stacktrace, 2))
@@ -403,68 +233,32 @@ defmodule IO do
@doc """
Inspects and writes the given `item` to the device.
It's important to note that it returns the given `item` unchanged.
This makes it possible to "spy" on values by inserting an
`IO.inspect/2` call almost anywhere in your code, for example,
in the middle of a pipeline.
It enables pretty printing by default with width of
80 characters. The width can be changed by explicitly
passing the `:width` option.
The output can be decorated with a label, by providing the `:label`
option to easily distinguish it from other `IO.inspect/2` calls.
The label will be printed before the inspected `item`.
See `Inspect.Opts` for a full list of remaining formatting options.
See `Inspect.Opts` for a full list of options.
## Examples
IO.inspect(<<0, 1, 2>>, width: 40)
Prints:
<<0, 1, 2>>
We can use the `:label` option to decorate the output:
IO.inspect(1..100, label: "a wonderful range")
Prints:
a wonderful range: 1..100
The `:label` option is especially useful with pipelines:
[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")
|> Enum.sum()
Prints:
before: [1, 2, 3]
after: [2, 4, 6]
IO.inspect Process.list, width: 40
"""
@spec inspect(item, keyword) :: item when item: var
@spec inspect(item, Keyword.t) :: item when item: var
def inspect(item, opts \\ []) do
inspect(:stdio, item, opts)
inspect group_leader(), item, opts
end
@doc """
Inspects `item` according to the given options using the IO `device`.
See `inspect/2` for a full list of options.
See `Inspect.Opts` for a full list of options.
"""
@spec inspect(device, item, keyword) :: item when item: var
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
opts = Inspect.Opts.new(opts)
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
chardata = Inspect.Algebra.format(doc, opts.width)
puts(device, [label, chardata])
@spec inspect(device, item, Keyword.t) :: item when item: var
def inspect(device, item, opts) when is_list(opts) do
opts = struct(Inspect.Opts, opts)
iodata = Inspect.Algebra.format(Inspect.Algebra.to_doc(item, opts), opts.width)
puts device, iodata
item
end
@@ -472,24 +266,18 @@ defmodule IO do
Gets a number of bytes from IO device `:stdio`.
If `:stdio` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved.
the number of Unicode codepoints to be retrieved.
Otherwise, `count` is the number of raw bytes to be retrieved.
See `IO.getn/3` for a description of return values.
"""
@spec getn(
device | chardata | String.Chars.t(),
pos_integer | :eof | 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, :eof) do
getn(:stdio, prompt, :eof)
end
def getn(prompt, count) when is_integer(count) and count > 0 do
getn(:stdio, prompt, count)
getn(group_leader, prompt, count)
end
def getn(device, prompt) when not is_integer(prompt) do
@@ -500,7 +288,7 @@ defmodule IO do
Gets a number of bytes from the IO `device`.
If the IO `device` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved.
the number of Unicode codepoints to be retrieved.
Otherwise, `count` is the number of raw bytes to be retrieved.
It returns:
@@ -514,28 +302,12 @@ defmodule IO do
NFS volume
"""
@spec getn(device, chardata | String.Chars.t(), pos_integer | :eof) :: chardata | nodata
def getn(device, prompt, :eof) do
getn_eof(map_dev(device), to_chardata(prompt), [])
end
@spec getn(device, chardata | String.Chars.t, pos_integer) :: chardata | nodata
def getn(device, prompt, count) when is_integer(count) and count > 0 do
:io.get_chars(map_dev(device), to_chardata(prompt), count)
end
defp getn_eof(device, prompt, acc) do
case :io.get_line(device, prompt) do
line when is_binary(line) or is_list(line) -> getn_eof(device, '', [line | acc])
:eof -> wrap_eof(:lists.reverse(acc))
other -> other
end
end
defp wrap_eof([h | _] = acc) when is_binary(h), do: IO.iodata_to_binary(acc)
defp wrap_eof([h | _] = acc) when is_list(h), do: :lists.flatten(acc)
defp wrap_eof([]), do: :eof
@doc ~S"""
@doc """
Reads a line from the IO `device`.
It returns:
@@ -553,25 +325,14 @@ defmodule IO do
To display "What is your name?" as a prompt and await user input:
IO.gets("What is your name?\n")
IO.gets "What is your name?\n"
"""
@spec gets(device, chardata | String.Chars.t()) :: chardata | nodata
def gets(device \\ :stdio, prompt) do
@spec gets(device, chardata | String.Chars.t) :: chardata | nodata
def gets(device \\ group_leader(), prompt) do
:io.get_line(map_dev(device), to_chardata(prompt))
end
@doc """
Returns a line-based `IO.Stream` on `:stdio`.
This is equivalent to:
IO.stream(:stdio, :line)
"""
@doc since: "1.12.0"
def stream, do: stream(:stdio, :line)
@doc """
Converts the IO `device` into an `IO.Stream`.
@@ -582,41 +343,27 @@ defmodule IO do
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
This reads from the IO as UTF-8. Check out
This reads from the IO as utf-8. Check out
`IO.binstream/2` to handle the IO as a raw binary.
Note that an IO stream has side effects and every time
you go over the stream you may get different results.
`stream/0` has been introduced in Elixir v1.12.0,
while `stream/2` has been available since v1.0.0.
## Examples
Here is an example on how we mimic an echo server
from the command line:
Enum.each(IO.stream(:stdio, :line), &IO.write(&1))
Enum.each IO.stream(:stdio, :line), &IO.write(&1)
"""
@spec stream(device, :line | pos_integer) :: Enumerable.t()
def stream(device \\ :stdio, line_or_codepoints)
@spec stream(device, :line | pos_integer) :: Enumerable.t
def stream(device, line_or_codepoints)
when line_or_codepoints == :line
when is_integer(line_or_codepoints) and line_or_codepoints > 0 do
IO.Stream.__build__(map_dev(device), false, line_or_codepoints)
end
@doc """
Returns a raw, line-based `IO.Stream` on `:stdio`. The operation is Unicode unsafe.
This is equivalent to:
IO.binstream(:stdio, :line)
"""
@doc since: "1.12.0"
def binstream, do: binstream(:stdio, :line)
@doc """
Converts the IO `device` into an `IO.Stream`. The operation is Unicode unsafe.
@@ -625,7 +372,8 @@ defmodule IO do
and write.
The `device` is iterated by the given number of bytes or line by line if
`:line` is given. This reads from the IO device as a raw binary.
`:line` is given.
This reads from the IO device as a raw binary.
Note that an IO stream has side effects and every time
you go over the stream you may get different results.
@@ -633,21 +381,17 @@ defmodule IO do
Finally, do not use this function on IO devices in Unicode
mode as it will return the wrong result.
`binstream/0` has been introduced in Elixir v1.12.0,
while `binstream/2` has been available since v1.0.0.
"""
@spec binstream(device, :line | pos_integer) :: Enumerable.t()
def binstream(device \\ :stdio, line_or_bytes)
@spec binstream(device, :line | pos_integer) :: Enumerable.t
def binstream(device, line_or_bytes)
when line_or_bytes == :line
when is_integer(line_or_bytes) and line_or_bytes > 0 do
IO.Stream.__build__(map_dev(device), true, line_or_bytes)
end
@doc """
Converts chardata into a string.
For more information about chardata, see the ["Chardata"](#module-chardata)
section in the module documentation.
Converts chardata (a list of integers representing codepoints,
lists and strings) into a string.
In case the conversion fails, it raises an `UnicodeConversionError`.
If a string is given, it returns the string itself.
@@ -664,9 +408,7 @@ defmodule IO do
"string"
"""
@spec chardata_to_string(chardata) :: String.t()
def chardata_to_string(chardata)
@spec chardata_to_string(chardata) :: String.t | no_return
def chardata_to_string(string) when is_binary(string) do
string
end
@@ -676,16 +418,14 @@ defmodule IO do
end
@doc """
Converts IO data into a binary
Converts iodata (a list of integers representing bytes, lists
and binaries) into a binary.
The operation is Unicode unsafe.
Note 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
IO data and chardata, see the ["IO data"](#module-io-data) section in the
module documentation.
Notice that this function treats lists of integers as raw bytes
and does not perform any kind of encoding conversion. If you want
to convert from a charlist to a string (UTF-8 encoded), please
use `chardata_to_string/1` instead.
If this function receives a binary, the same binary is returned.
@@ -705,15 +445,12 @@ defmodule IO do
"""
@spec iodata_to_binary(iodata) :: binary
def iodata_to_binary(iodata) do
:erlang.iolist_to_binary(iodata)
def iodata_to_binary(item) do
:erlang.iolist_to_binary(item)
end
@doc """
Returns the size of an IO data.
For more information about IO data, see the ["IO data"](#module-io-data)
section in the module documentation.
Returns the size of an iodata.
Inlined by the compiler.
@@ -724,8 +461,8 @@ defmodule IO do
"""
@spec iodata_length(iodata) :: non_neg_integer
def iodata_length(iodata) do
:erlang.iolist_size(iodata)
def iodata_length(item) do
:erlang.iolist_size(item)
end
@doc false
@@ -733,10 +470,8 @@ defmodule IO do
case read(device, line_or_codepoints) do
:eof ->
{:halt, device}
{:error, reason} ->
raise IO.StreamError, reason: reason
data ->
{[data], device}
end
@@ -747,10 +482,8 @@ defmodule IO do
case binread(device, line_or_chars) do
:eof ->
{:halt, device}
{:error, reason} ->
raise IO.StreamError, reason: reason
data ->
{[data], device}
end
@@ -759,7 +492,7 @@ defmodule IO do
@compile {:inline, map_dev: 1, to_chardata: 1}
# Map the Elixir names for standard IO and error to Erlang names
defp map_dev(:stdio), do: :standard_io
defp map_dev(:stdio), do: :standard_io
defp map_dev(:stderr), do: :standard_error
defp map_dev(other) when is_atom(other) or is_pid(other) or is_tuple(other), do: other
+77 -173
View File
@@ -3,7 +3,6 @@ defmodule IO.ANSI.Sequence do
defmacro defsequence(name, code, terminator \\ "m") do
quote bind_quoted: [name: name, code: code, terminator: terminator] do
@spec unquote(name)() :: String.t()
def unquote(name)() do
"\e[#{unquote(code)}#{unquote(terminator)}"
end
@@ -22,39 +21,13 @@ 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
@type ansicode :: atom
@type ansilist ::
maybe_improper_list(char | ansicode | binary | ansilist, binary | ansicode | [])
@type ansidata :: ansilist | ansicode | binary
@typep ansicode :: atom
@typep ansilist :: maybe_improper_list(char | ansicode | binary | ansilist, binary | ansicode | [])
@type ansidata :: ansilist | ansicode | binary
@doc """
Checks if ANSI coloring is supported and enabled on this machine.
@@ -69,36 +42,8 @@ defmodule IO.ANSI do
Application.get_env(:elixir, :ansi_enabled, false)
end
@doc """
Syntax colors to be used by `Inspect`.
Those colors are used throughout Elixir's standard library,
such as `dbg/2` and `IEx`.
The colors can be changed by setting the `:ansi_syntax_colors`
in the `:elixir` application configuration. Configuration for
most built-in data types are supported: `:atom`, `:binary`,
`:boolean`, `:charlist`, `:list`, `:map`, `:nil`, `:number`,
`:string`, and `:tuple`. The default is:
[
atom: :cyan
boolean: :magenta,
charlist: :yellow,
nil: :magenta,
number: :yellow,
string: :green
]
"""
@doc since: "1.14.0"
@spec syntax_colors :: Keyword.t(ansidata)
def syntax_colors do
Application.fetch_env!(:elixir, :ansi_syntax_colors)
end
@doc "Sets foreground color."
@spec color(0..255) :: String.t()
@doc "Sets foreground color"
@spec color(0..255) :: String.t
def color(code) when code in 0..255, do: "\e[38;5;#{code}m"
@doc ~S"""
@@ -106,13 +51,13 @@ defmodule IO.ANSI do
Valid values for each color are in the range 0 to 5.
"""
@spec color(0..5, 0..5, 0..5) :: String.t()
@spec color(0..5, 0..5, 0..5) :: String.t
def color(r, g, b) when r in 0..5 and g in 0..5 and b in 0..5 do
color(16 + 36 * r + 6 * g + b)
color(16 + (36 * r) + (6 * g) + b)
end
@doc "Sets background color."
@spec color_background(0..255) :: String.t()
@doc "Sets background color"
@spec color_background(0..255) :: String.t
def color_background(code) when code in 0..255, do: "\e[48;5;#{code}m"
@doc ~S"""
@@ -120,145 +65,106 @@ defmodule IO.ANSI do
Valid values for each color are in the range 0 to 5.
"""
@spec color_background(0..5, 0..5, 0..5) :: String.t()
@spec color_background(0..5, 0..5, 0..5) :: String.t
def color_background(r, g, b) when r in 0..5 and g in 0..5 and b in 0..5 do
color_background(16 + 36 * r + 6 * g + b)
color_background(16 + (36 * r) + (6 * g) + b)
end
@doc "Resets all attributes."
defsequence(:reset, 0)
@doc "Resets all attributes"
defsequence :reset, 0
@doc "Bright (increased intensity) or bold."
defsequence(:bright, 1)
@doc "Bright (increased intensity) or Bold"
defsequence :bright, 1
@doc "Faint (decreased intensity). Not widely supported."
defsequence(:faint, 2)
@doc "Faint (decreased intensity), not widely supported"
defsequence :faint, 2
@doc "Italic: on. Not widely supported. Sometimes treated as inverse."
defsequence(:italic, 3)
@doc "Italic: on. Not widely supported. Sometimes treated as inverse"
defsequence :italic, 3
@doc "Underline: single."
defsequence(:underline, 4)
@doc "Underline: Single"
defsequence :underline, 4
@doc "Blink: slow. Less than 150 per minute."
defsequence(:blink_slow, 5)
@doc "Blink: Slow. Less than 150 per minute"
defsequence :blink_slow, 5
@doc "Blink: rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported."
defsequence(:blink_rapid, 6)
@doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
defsequence :blink_rapid, 6
@doc "Image: negative. Swap foreground and background."
defsequence(:inverse, 7)
@doc "Image: Negative. Swap foreground and background"
defsequence :inverse, 7
@doc "Image: negative. Swap foreground and background."
defsequence(:reverse, 7)
@doc "Image: Negative. Swap foreground and background"
defsequence :reverse, 7
@doc "Conceal. Not widely supported."
defsequence(:conceal, 8)
@doc "Conceal. Not widely supported"
defsequence :conceal, 8
@doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
defsequence(:crossed_out, 9)
@doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported"
defsequence :crossed_out, 9
@doc "Sets primary (default) font."
defsequence(:primary_font, 10)
@doc "Sets primary (default) font"
defsequence :primary_font, 10
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
@doc "Sets alternative font #{font_n}."
defsequence(:"font_#{font_n}", font_n + 10)
@doc "Sets alternative font #{font_n}"
defsequence :"font_#{font_n}", font_n + 10
end
@doc "Normal color or intensity."
defsequence(:normal, 22)
@doc "Normal color or intensity"
defsequence :normal, 22
@doc "Not italic."
defsequence(:not_italic, 23)
@doc "Not italic"
defsequence :not_italic, 23
@doc "Underline: none."
defsequence(:no_underline, 24)
@doc "Underline: None"
defsequence :no_underline, 24
@doc "Blink: off."
defsequence(:blink_off, 25)
@doc "Image: positive. Normal foreground and background."
defsequence(:inverse_off, 27)
@doc "Image: positive. Normal foreground and background."
defsequence(:reverse_off, 27)
@doc "Blink: off"
defsequence :blink_off, 25
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
for {color, code} <- Enum.with_index(colors) do
@doc "Sets foreground color to #{color}."
defsequence(color, code + 30)
@doc "Sets foreground color to #{color}"
defsequence color, code + 30
@doc "Sets foreground color to light #{color}."
defsequence(:"light_#{color}", code + 90)
@doc "Sets background color to #{color}."
defsequence(:"#{color}_background", code + 40)
@doc "Sets background color to light #{color}."
defsequence(:"light_#{color}_background", code + 100)
@doc "Sets background color to #{color}"
defsequence :"#{color}_background", code + 40
end
@doc "Default text color."
defsequence(:default_color, 39)
@doc "Default text color"
defsequence :default_color, 39
@doc "Default background color."
defsequence(:default_background, 49)
@doc "Default background color"
defsequence :default_background, 49
@doc "Framed."
defsequence(:framed, 51)
@doc "Framed"
defsequence :framed, 51
@doc "Encircled."
defsequence(:encircled, 52)
@doc "Encircled"
defsequence :encircled, 52
@doc "Overlined."
defsequence(:overlined, 53)
@doc "Overlined"
defsequence :overlined, 53
@doc "Not framed or encircled."
defsequence(:not_framed_encircled, 54)
@doc "Not framed or encircled"
defsequence :not_framed_encircled, 54
@doc "Not overlined."
defsequence(:not_overlined, 55)
@doc "Not overlined"
defsequence :not_overlined, 55
@doc "Sends cursor home."
defsequence(:home, "", "H")
@doc "Sends cursor home"
defsequence :home, "", "H"
@doc """
Sends cursor to the absolute position specified by `line` and `column`.
@doc "Clears screen"
defsequence :clear, "2", "J"
Line `0` and column `0` would mean the top left corner.
"""
@spec cursor(non_neg_integer, non_neg_integer) :: String.t()
def cursor(line, column)
when is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
"\e[#{line};#{column}H"
end
@doc "Sends cursor `lines` up."
@spec cursor_up(pos_integer) :: String.t()
def cursor_up(lines \\ 1) when is_integer(lines) and lines >= 1, do: "\e[#{lines}A"
@doc "Sends cursor `lines` down."
@spec cursor_down(pos_integer) :: String.t()
def cursor_down(lines \\ 1) when is_integer(lines) and lines >= 1, do: "\e[#{lines}B"
@doc "Sends cursor `columns` to the right."
@spec cursor_right(pos_integer) :: String.t()
def cursor_right(columns \\ 1) when is_integer(columns) and columns >= 1, do: "\e[#{columns}C"
@doc "Sends cursor `columns` to the left."
@spec cursor_left(pos_integer) :: String.t()
def cursor_left(columns \\ 1) when is_integer(columns) and columns >= 1, do: "\e[#{columns}D"
@doc "Clears screen."
defsequence(:clear, "2", "J")
@doc "Clears line."
defsequence(:clear_line, "2", "K")
@doc "Clears line"
defsequence :clear_line, "2", "K"
defp format_sequence(other) do
raise ArgumentError, "invalid ANSI sequence specification: #{inspect(other)}"
raise ArgumentError, "invalid ANSI sequence specification: #{inspect other}"
end
@doc ~S"""
@@ -271,7 +177,7 @@ defmodule IO.ANSI do
performed. If you don't want this behaviour, use `format_fragment/2`.
An optional boolean parameter can be passed to enable or disable
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
By default checks if ANSI is enabled using the `enabled?/0` function.
## Examples
@@ -280,9 +186,8 @@ defmodule IO.ANSI do
[[[[[[], "Hello, "] | "\e[31m"] | "\e[1m"], "world!"] | "\e[0m"]
"""
@spec format(ansidata, boolean) :: IO.chardata()
def format(ansidata, emit? \\ enabled?()) when is_boolean(emit?) do
do_format(ansidata, [], [], emit?, :maybe)
def format(chardata, emit? \\ enabled?) when is_boolean(emit?) do
do_format(chardata, [], [], emit?, :maybe)
end
@doc ~S"""
@@ -292,7 +197,7 @@ defmodule IO.ANSI do
The named sequences are represented by atoms.
An optional boolean parameter can be passed to enable or disable
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
By default checks if ANSI is enabled using the `enabled?/0` function.
## Examples
@@ -301,9 +206,8 @@ defmodule IO.ANSI do
[[[[[[] | "\e[1m"], 87], 111], 114], 100]
"""
@spec format_fragment(ansidata, boolean) :: IO.chardata()
def format_fragment(ansidata, emit? \\ enabled?()) when is_boolean(emit?) do
do_format(ansidata, [], [], emit?, false)
def format_fragment(chardata, emit? \\ enabled?) when is_boolean(emit?) do
do_format(chardata, [], [], emit?, false)
end
defp do_format([term | rest], rem, acc, emit?, append_reset) do
@@ -327,7 +231,7 @@ defmodule IO.ANSI do
end
defp do_format([], [], acc, true, true) do
[acc | IO.ANSI.reset()]
[acc | IO.ANSI.reset]
end
defp do_format([], [], acc, _emit?, _append_reset) do
+150 -554
View File
@@ -1,46 +1,36 @@
defmodule IO.ANSI.Docs do
@moduledoc false
@bullet_text_unicode "• "
@bullet_text_ascii "* "
@bullets [?*, ?-, ?+]
@spaces [" ", "\n", "\t"]
@doc """
The default options used by this module.
The supported keys are:
The supported values are:
* `:enabled` - toggles coloring on and off (true)
* `:doc_bold` - bold text (bright)
* `:doc_code` - code blocks (cyan)
* `:doc_headings` - h1, h2, h3, h4, h5, h6 headings (yellow)
* `:doc_metadata` - documentation metadata keys (yellow)
* `:doc_quote` - leading quote character `> ` (light black)
* `:doc_code` - code blocks (cyan, bright)
* `:doc_headings` - h1 and h2 headings (yellow, bright)
* `:doc_inline_code` - inline code (cyan)
* `:doc_table_heading` - the style for table headings
* `:doc_title` - top level heading (reverse, yellow)
* `:doc_table_heading` - style for table headings
* `:doc_title` - top level heading (reverse, yellow, bright)
* `:doc_underline` - underlined text (underline)
* `:width` - the width to format the text (80)
Values for the color settings are strings with
comma-separated ANSI values.
"""
@spec default_options() :: keyword
def default_options do
[
enabled: true,
doc_bold: [:bright],
doc_code: [:cyan],
doc_headings: [:yellow],
doc_metadata: [:yellow],
doc_quote: [:light_black],
doc_inline_code: [:cyan],
doc_table_heading: [:reverse],
doc_title: [:reverse, :yellow],
doc_underline: [:underline],
width: 80
]
[enabled: true,
doc_bold: [:bright],
doc_code: [:cyan, :bright],
doc_headings: [:yellow],
doc_inline_code: [:cyan],
doc_table_heading: [:reverse],
doc_title: [:reverse, :yellow],
doc_underline: [:underline],
width: 80]
end
@doc """
@@ -48,267 +38,24 @@ 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
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)
def print_heading(heading, options \\ []) do
IO.puts IO.ANSI.reset
options = Keyword.merge(default_options, options)
width = options[:width]
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 """
Prints documentation metadata (only `delegate_to`, `deprecated`, `guard`, and `since` for now).
Prints the documentation body.
See `default_options/0` for docs on the supported options.
In addition to the printing string, takes a set of options
defined in `default_options/1`.
"""
@spec print_metadata(map, keyword) :: :ok
def print_metadata(metadata, options \\ []) when is_map(metadata) do
options = Keyword.merge(default_options(), options)
print_each_metadata(metadata, options) && IO.write("\n")
end
@metadata_filter [:deprecated, :guard, :since]
defp print_each_metadata(metadata, options) do
metadata
|> Enum.sort()
|> Enum.reduce(false, fn
{key, value}, _printed when is_binary(value) and key in @metadata_filter ->
label = metadata_label(key, options)
indent = String.duplicate(" ", length_without_escape(label, 0) + 1)
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)])
{:delegate_to, {m, f, a}}, _printed ->
label = metadata_label(:delegate_to, options)
IO.puts([label, ?\s, Exception.format_mfa(m, f, a)])
_metadata, printed ->
printed
end)
end
defp metadata_label(key, options) do
"#{color(:doc_metadata, options)}#{key}:#{maybe_reset(options)}"
end
@doc """
Prints the documentation body `doc` according to `format`.
It 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({tag, _, entries}, indent, options) when tag in [:strong, :b] 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(options) | 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(options) | 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, :strong, :b, :br], do: true
defp inline_html?(_), do: false
## Markdown
def print_markdown(doc, options) do
options = Keyword.merge(default_options(), options)
def print(doc, options \\ []) do
options = Keyword.merge(default_options, options)
doc
|> String.split(["\r\n", "\n"], trim: false)
|> Enum.map(&String.trim_trailing/1)
@@ -319,33 +66,22 @@ defmodule IO.ANSI.Docs do
write_text(text, indent, options)
end
defp process(["# " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["## " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["#### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["##### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["###### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process([">" <> line | rest], text, indent, options) do
defp process(["# " <> heading | rest], text, indent, options) do
write_text(text, indent, options)
process_quote(rest, [line], indent, options)
write_h1(String.trim(heading), options)
process(rest, [], "", options)
end
defp process(["## " <> heading | rest], text, indent, options) do
write_text(text, indent, options)
write_h2(String.trim(heading), options)
process(rest, [], "", options)
end
defp process(["### " <> heading | rest], text, indent, options) do
write_text(text, indent, options)
write_h3(String.trim(heading), indent, options)
process(rest, [], "", options)
end
defp process(["" | rest], text, indent, options) do
@@ -366,87 +102,50 @@ defmodule IO.ANSI.Docs do
process_fenced_code_block(rest, text, indent, options, _delimiter = "~~~")
end
defp process(all = [line | rest], text, indent, options) do
defp process(all=[line | rest], text, indent, options) do
{stripped, count} = strip_spaces(line, 0, :infinity)
cond do
link_label?(stripped, count) ->
write_text([line], indent, options, true)
process(rest, text, indent, options)
table_line?(stripped) and rest != [] and table_line?(hd(rest)) ->
write_text(text, indent, options)
process_table(all, indent, options)
true ->
process_rest(stripped, rest, count, text, indent, options)
end
end
### Headings
## Headings
defp write_heading(heading, rest, text, indent, options) do
write_text(text, indent, options)
defp write_h1(heading, options) do
write_h2(String.upcase(heading), options)
end
defp write_h2(heading, options) do
write(:doc_headings, heading, options)
newline_after_block(options)
process(rest, [], "", options)
newline_after_block
end
### Quotes
defp process_quote([], lines, indent, options) do
write_quote(lines, indent, options, false)
defp write_h3(heading, indent, options) do
IO.write(indent)
write(:doc_headings, heading, options)
newline_after_block
end
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
write_quote(lines, indent, options, true)
write_empty_quote_line(options)
process_quote(rest, [line], indent, options)
end
defp process_quote([">" <> line | rest], lines, indent, options) do
process_quote(rest, [line | lines], indent, options)
end
defp process_quote(rest, lines, indent, options) do
write_quote(lines, indent, options, false)
process(rest, [], indent, options)
end
defp write_quote(lines, indent, options, no_wrap) do
lines
|> Enum.map(&String.trim/1)
|> Enum.reverse()
|> write_lines(
indent,
options,
no_wrap,
quote_prefix(options)
)
end
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(options), 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)
process_list(<<d1, ?., ?\s>>, item, rest, count, indent, options)
<<d1, d2, ?., ?\s, item::binary>> when d1 in ?0..?9 and d2 in ?0..?9 ->
write_text(text, indent, options)
process_list(<<d1, d2, ?., ?\s>>, item, rest, count, indent, options)
_ ->
process(rest, [stripped | text], indent, options)
end
@@ -457,18 +156,15 @@ 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
defp process_list_next([line | rest], count, max, acc) do
{stripped, next_count} = strip_spaces(line, 0, max)
case process_list_next_kind(stripped, rest, count, next_count) do
:next -> process_list_next(rest, count, max, [stripped | acc])
:done -> {Enum.reverse(acc), [line | rest], true}
@@ -484,26 +180,20 @@ defmodule IO.ANSI.Docs do
case {stripped, rest} do
{<<bullet, ?\s, _::binary>>, _} when bullet in @bullets and next_count <= count ->
:list
{<<d1, ?., ?\s, _::binary>>, _} when d1 in ?0..?9 and next_count <= count ->
:list
{<<d1, d2, ?., ?\s, _::binary>>, _}
when d1 in ?0..?9 and d2 in ?0..?9 and next_count <= count ->
{<<d1, d2, ?., ?\s, _::binary>>, _} when d1 in ?0..?9 and d2 in ?0..?9 and next_count <= count ->
:list
{"", [" " <> _ | _]} ->
:next
{"", _} ->
:done
_ ->
:next
end
end
### Text
## Text
defp write_text(text, indent, options) do
case Enum.reverse(text) do
@@ -517,26 +207,17 @@ defmodule IO.ANSI.Docs do
end
defp write_text(lines, indent, options, no_wrap) do
write_lines(lines, indent, options, no_wrap, "")
end
defp write_lines(lines, indent, options, no_wrap, prefix) do
lines
|> Enum.join(" ")
|> format_text(options)
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
unless no_wrap, do: newline_after_block(options)
end
defp format_text(text, options) do
text
|> handle_links()
|> handle_links
|> handle_inline(options)
|> String.split(~r{\s})
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap)
unless no_wrap, do: newline_after_block()
end
### Code blocks
## Code blocks
defp process_code([], code, indent, options) do
write_code(code, indent, options)
@@ -574,53 +255,47 @@ defmodule IO.ANSI.Docs do
end
defp write_code(code, indent, options) do
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
newline_after_block(options)
write(:doc_code, "#{indent}┃ #{Enum.join(Enum.reverse(code), "\n#{indent}┃ ")}", 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
defp table_lines(lines, options) do
lines = Enum.map(lines, &split_into_columns(&1, options))
count = Enum.map(lines, &length/1) |> Enum.max()
count = Enum.map(lines, &length/1) |> Enum.max
lines = Enum.map(lines, &pad_to_number_of_columns(&1, count))
widths =
for line <- lines do
if table_header?(line) do
for _ <- line, do: 0
else
for {_col, length} <- line, do: length
end
end
widths = for line <- lines, do:
(for {_col, length} <- line, do: length)
col_widths = Enum.reduce(widths,
List.duplicate(0, count),
&max_column_widths/2)
col_widths = Enum.reduce(widths, List.duplicate(0, count), &max_column_widths/2)
render_table(lines, col_widths, options)
end
defp split_into_columns(line, options) do
line
|> String.trim(" ")
|> String.trim("|")
|> String.split(~r{(?<!\\)\|})
|> String.trim()
|> String.split(~r/\s\|\s/)
|> Enum.map(&render_column(&1, options))
end
defp render_column(col, options) do
col =
col
|> String.trim()
|> String.replace("\\\|", "|")
|> handle_links
|> handle_inline(options)
col = col
|> String.replace(~r/\\ \|/x, "|")
|> handle_links
|> handle_inline(options)
{col, length_without_escape(col, 0)}
end
@@ -633,10 +308,7 @@ defmodule IO.ANSI.Docs do
# If second line is heading separator, use the heading style on the first
defp render_table([first, second | rest], widths, options) do
combined = Enum.zip(first, widths)
if table_header?(second) do
alignments = Enum.map(second, &column_alignment/1)
options = Keyword.put_new(options, :alignments, alignments)
draw_table_row(combined, options, :heading)
render_table(rest, widths, options)
else
@@ -651,65 +323,31 @@ defmodule IO.ANSI.Docs do
render_table(rest, widths, options)
end
defp render_table([], _, _), do: nil
defp render_table([], _, _),
do: nil
defp column_alignment({line, _}) do
cond do
String.starts_with?(line, ":") and String.ends_with?(line, ":") -> :center
String.ends_with?(line, ":") -> :right
true -> :left
end
end
defp table_header?(line) do
Enum.all?(line, fn {col, _} -> table_header_column?(col) end)
end
defp table_header_column?(":" <> rest), do: table_header_contents?(rest)
defp table_header_column?(col), do: table_header_contents?(col)
defp table_header_contents?("-" <> rest), do: table_header_contents?(rest)
defp table_header_contents?(":"), do: true
defp table_header_contents?(""), do: true
defp table_header_contents?(_), do: false
defp table_header?(row), do:
Enum.all?(row, fn {col, _} -> col =~ ~r/^:?-+:?$/ end)
defp draw_table_row(cols_and_widths, options, heading \\ false) do
default_alignments = List.duplicate(:left, length(cols_and_widths))
alignments = Keyword.get(options, :alignments, default_alignments)
columns =
cols_and_widths
|> Enum.zip(alignments)
|> Enum.map_join(" | ", &generate_table_cell/1)
Enum.map_join(cols_and_widths, " | ", fn {{col, length}, width} ->
col <> String.duplicate(" ", width - length)
end)
if heading do
write(:doc_table_heading, columns, options)
else
IO.puts(columns)
IO.puts columns
end
end
defp generate_table_cell({{{col, length}, width}, :center}) do
ansi_diff = byte_size(col) - length
width = width + ansi_diff
col
|> String.pad_leading(div(width, 2) - div(length, 2) + length)
|> String.pad_trailing(width + 1 - rem(width, 2))
end
defp generate_table_cell({{{col, length}, width}, :right}) do
ansi_diff = byte_size(col) - length
String.pad_leading(col, width + ansi_diff)
end
defp generate_table_cell({{{col, length}, width}, :left}) do
ansi_diff = byte_size(col) - length
String.pad_trailing(col, width + ansi_diff)
end
defp table_line?(line) do
line =~ ~r/[:\ -]\|[:\ -]/
Regex.match?(~r'''
( ^ \s{0,3} \| (?: [^|]+ \|)+ \s* $ )
|
(\s \| \s)
'''x, line)
end
## Helpers
@@ -722,34 +360,23 @@ defmodule IO.ANSI.Docs do
defp link_label?(""), do: false
defp link_label?(<<_>> <> rest), do: link_label?(rest)
defp strip_spaces(" " <> line, acc, max) when acc < max, do: strip_spaces(line, acc + 1, max)
defp strip_spaces(rest, acc, _max), do: {rest, acc}
defp strip_spaces(" " <> line, acc, max) when acc < max,
do: strip_spaces(line, acc + 1, max)
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
defp write_with_wrap([], _available, _indent, _first) do
:ok
end
defp write_with_wrap(words, available, indent, first, prefix) do
words
|> wrap_text(available, indent, first, prefix, [])
|> tl()
|> IO.puts()
end
defp wrap_text([], _available, _indent, _first, _prefix, wrapped_lines) do
Enum.reverse(wrapped_lines)
end
defp wrap_text(words, available, indent, first, prefix, wrapped_lines) do
prefix_length = length_without_escape(prefix, 0)
{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])
defp write_with_wrap(words, available, indent, first) do
{words, rest} = take_words(words, available, [])
IO.puts (if first, do: "", else: indent) <> Enum.join(words, " ")
write_with_wrap(rest, available, indent, false)
end
defp take_words([word | words], available, acc) do
@@ -796,22 +423,21 @@ defmodule IO.ANSI.Docs do
end
defp escape_underlines_in_link(text) do
# Regular expression adapted from https://tools.ietf.org/html/rfc3986#appendix-B
Regex.replace(~r{[a-z][a-z0-9\+\-\.]*://\S*}i, text, &String.replace(&1, "_", "\\_"))
Regex.replace(~r{https?\S*}, text, &String.replace(&1, "_", "\\_"))
end
defp remove_square_brackets_in_link(text) do
Regex.replace(~r{\[([^\]]*?)\]\((.*?)\)}, text, "\\1 (\\2)")
Regex.replace(~r{\[(.*?)\]\((.*?)\)}, text, "\\1 (\\2)")
end
# We have four entries: **, __, *, _ and `.
# We have four entries: **, *, _ and `.
#
# The first four behave the same while the last one is simpler
# when it comes to delimiters as it ignores spaces and escape
# characters. But, since the first two has two characters,
# we need to handle 3 cases:
# The first three behave the same while the last one is simpler
# when it comes to delimiters. But, since the first has two
# characters, we need to handle 3 cases:
#
# 1. __ and **
# 1. **
# 2. _ and *
# 3. `
#
@@ -820,13 +446,12 @@ defmodule IO.ANSI.Docs do
# Characters that can mark the beginning or the end of a word.
# Only support the most common ones at this moment.
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&, ?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
### Inline start
# Inline start
defp handle_inline(<<mark, mark, rest::binary>>, options) when mark in @single do
handle_inline(rest, [mark | mark], [<<mark, mark>>], [], options)
defp handle_inline(<<?*, ?*, rest::binary>>, options) do
handle_inline(rest, ?d, ["**"], [], options)
end
defp handle_inline(<<mark, rest::binary>>, options) when mark in @single do
@@ -837,34 +462,32 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, nil, [], [], options)
end
### Inline delimiters
# Inline delimiters
defp handle_inline(<<delimiter, mark, mark, rest::binary>>, nil, buffer, acc, options)
when rest != "" and delimiter in @delimiters and mark in @single do
acc = [delimiter, Enum.reverse(buffer) | acc]
handle_inline(rest, [mark | mark], [<<mark, mark>>], acc, options)
defp handle_inline(<<delimiter, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
when rest != "" and delimiter in @delimiters do
handle_inline(rest, ?d, ["**"], [delimiter, Enum.reverse(buffer) | acc], options)
end
defp handle_inline(<<delimiter, mark, rest::binary>>, nil, buffer, acc, options)
when rest != "" and delimiter in @delimiters and mark in @single do
when rest != "" and delimiter in @delimiters and mark in @single do
handle_inline(rest, mark, [<<mark>>], [delimiter, Enum.reverse(buffer) | acc], options)
end
defp handle_inline(<<?`, rest::binary>>, nil, buffer, acc, options)
when rest != "" do
when rest != "" do
handle_inline(rest, ?`, ["`"], [Enum.reverse(buffer) | acc], options)
end
### Clauses for handling escape
# Clauses for handling escape
defp handle_inline(<<?\\, ?\\, mark, mark, rest::binary>>, nil, buffer, acc, options)
when rest != "" and mark in @single do
acc = [?\\, Enum.reverse(buffer) | acc]
handle_inline(rest, [mark | mark], [<<mark, mark>>], acc, options)
defp handle_inline(<<?\\, ?\\, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
when rest != "" do
handle_inline(rest, ?d, ["**"], [?\\, Enum.reverse(buffer) | acc], options)
end
defp handle_inline(<<?\\, ?\\, mark, rest::binary>>, nil, buffer, acc, options)
when rest != "" and mark in @single do
when rest != "" and mark in @single do
handle_inline(rest, mark, [<<mark>>], [?\\, Enum.reverse(buffer) | acc], options)
end
@@ -873,31 +496,30 @@ defmodule IO.ANSI.Docs do
end
# An escape is not valid inside `
defp handle_inline(<<?\\, mark, rest::binary>>, limit, buffer, acc, options) when limit != ?` do
defp handle_inline(<<?\\, mark, rest::binary>>, limit, buffer, acc, options)
when not(mark == limit and mark == ?`) do
handle_inline(rest, limit, [mark | buffer], acc, options)
end
### Inline end
# Inline end
defp handle_inline(<<mark, mark, delimiter, rest::binary>>, [mark | mark], buffer, acc, options)
when delimiter in @delimiters and mark in @single do
inline_buffer = inline_buffer(buffer, options)
handle_inline(<<delimiter, rest::binary>>, nil, [], [inline_buffer | acc], options)
defp handle_inline(<<?*, ?*, delimiter, rest::binary>>, ?d, buffer, acc, options)
when delimiter in @delimiters do
handle_inline(<<delimiter, rest::binary>>, nil, [], [inline_buffer(buffer, options) | acc], options)
end
defp handle_inline(<<mark, delimiter, rest::binary>>, mark, buffer, acc, options)
when delimiter in @delimiters and mark in @single do
inline_buffer = inline_buffer(buffer, options)
handle_inline(<<delimiter, rest::binary>>, nil, [], [inline_buffer | acc], options)
when delimiter in @delimiters and mark in @single do
handle_inline(<<delimiter, rest::binary>>, nil, [], [inline_buffer(buffer, options) | acc], options)
end
defp handle_inline(<<mark, mark, rest::binary>>, [mark | mark], buffer, acc, options)
when rest == "" and mark in @single do
defp handle_inline(<<?*, ?*, rest::binary>>, ?d, buffer, acc, options)
when rest == "" do
handle_inline(<<>>, nil, [], [inline_buffer(buffer, options) | acc], options)
end
defp handle_inline(<<mark, rest::binary>>, mark, buffer, acc, options)
when rest == "" and mark in @single do
when rest == "" and mark in @single do
handle_inline(<<>>, nil, [], [inline_buffer(buffer, options) | acc], options)
end
@@ -905,60 +527,34 @@ 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)
end
defp handle_inline(<<>>, _mark, buffer, acc, _options) do
IO.iodata_to_binary(Enum.reverse([Enum.reverse(buffer) | acc]))
IO.iodata_to_binary Enum.reverse([Enum.reverse(buffer) | acc])
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
case mark do
"__" -> color(:doc_bold, colors)
"`" -> color(:doc_inline_code, colors)
"_" -> color(:doc_underline, colors)
"*" -> color(:doc_bold, colors)
"**" -> color(:doc_bold, colors)
"_" -> color(:doc_underline, colors)
"*" -> color(:doc_underline, colors)
"`" -> color(:doc_inline_code, colors)
end
end
defp bullet_text(options) do
if options[:enabled], do: @bullet_text_unicode, else: @bullet_text_ascii
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
+7 -19
View File
@@ -1,9 +1,8 @@
defmodule IO.StreamError do
defexception [:reason, :message]
@impl true
def exception(opts) do
reason = opts[:reason]
reason = opts[:reason]
formatted = IO.iodata_to_binary(:file.format_error(reason))
%IO.StreamError{message: "error during streaming: #{formatted}", reason: reason}
end
@@ -17,7 +16,7 @@ defmodule IO.Stream do
* `device` - the IO device
* `raw` - a boolean indicating if bin functions should be used
* `line_or_bytes` - if reading should read lines or a given number of bytes
* `line_or_bytes` - if reading should read lines or a given amount of bytes
It is worth noting that an IO stream has side effects and every time you go
over the stream you may get different results.
@@ -26,11 +25,7 @@ defmodule IO.Stream do
defstruct device: nil, raw: true, line_or_bytes: :line
@type t :: %__MODULE__{
device: IO.device(),
raw: boolean(),
line_or_bytes: :line | non_neg_integer()
}
@type t :: %__MODULE__{}
@doc false
def __build__(device, raw, line_or_bytes) do
@@ -46,12 +41,10 @@ defmodule IO.Stream do
fn
:ok, {:cont, x} ->
case raw do
true -> IO.binwrite(device, x)
true -> IO.binwrite(device, x)
false -> IO.write(device, x)
end
:ok, _ ->
stream
:ok, _ -> stream
end
end
end
@@ -60,11 +53,10 @@ defmodule IO.Stream do
def reduce(%{device: device, raw: raw, line_or_bytes: line_or_bytes}, acc, fun) do
next_fun =
case raw do
true -> &IO.each_binstream(&1, line_or_bytes)
true -> &IO.each_binstream(&1, line_or_bytes)
false -> &IO.each_stream(&1, line_or_bytes)
end
Stream.resource(fn -> device end, next_fun, & &1).(acc, fun)
Stream.resource(fn -> device end, next_fun, &(&1)).(acc, fun)
end
def count(_stream) do
@@ -74,9 +66,5 @@ defmodule IO.Stream do
def member?(_stream, _term) do
{:error, __MODULE__}
end
def slice(_stream) do
{:error, __MODULE__}
end
end
end
+1247 -3390
View File
File diff suppressed because it is too large Load Diff
+107 -245
View File
@@ -1,23 +1,9 @@
defmodule Kernel.CLI do
@moduledoc false
@compile {:no_warn_undefined, [Logger, IEx]}
@blank_config %{
commands: [],
output: ".",
compile: [],
no_halt: false,
compiler_options: [],
errors: [],
pa: [],
pz: [],
verbose_compile: false,
profile: nil,
pry: false
}
@standalone_opts ["-h", "--help", "--short-version"]
@blank_config %{commands: [], output: ".", compile: [],
halt: true, compiler_options: [], errors: [],
pa: [], pz: [], verbose_compile: false}
@doc """
This is the API invoked by Elixir boot process.
@@ -27,22 +13,15 @@ defmodule Kernel.CLI do
{config, argv} = parse_argv(argv)
System.argv(argv)
System.no_halt(config.no_halt)
if config.pry do
Application.put_env(:elixir, :dbg_callback, {IEx.Pry, :dbg, []})
end
fun = fn _ ->
run fn _ ->
errors = process_commands(config)
if errors != [] do
Enum.each(errors, &IO.puts(:stderr, &1))
System.halt(1)
end
end
run(fun)
end, config.halt
end
@doc """
@@ -53,10 +32,9 @@ defmodule Kernel.CLI do
This function is used by Elixir's CLI and also
by escripts generated by Elixir.
"""
def run(fun) do
def run(fun, halt \\ true) do
{ok_or_shutdown, status} = exec_fun(fun, {:ok, 0})
if ok_or_shutdown == :shutdown or not System.no_halt() do
if ok_or_shutdown == :shutdown or halt do
{_, status} = at_exit({ok_or_shutdown, status})
# Ensure Logger messages are flushed before halting
@@ -69,51 +47,18 @@ defmodule Kernel.CLI do
end
end
@doc """
Parses the CLI arguments. Made public for testing.
"""
@doc false
def parse_argv(argv) do
parse_argv(argv, @blank_config)
end
@doc """
Process CLI commands. Made public for testing.
"""
@doc false
def process_commands(config) do
results = Enum.map(Enum.reverse(config.commands), &process_command(&1, config))
errors = for {:error, msg} <- results, do: msg
errors = for {:error, msg} <- results, do: msg
Enum.reverse(config.errors, errors)
end
@doc """
Shared helper for error formatting on CLI tools.
"""
def format_error(kind, reason, stacktrace) do
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
iodata =
case blamed do
%FunctionClauseError{} ->
formatted = Exception.format_banner(kind, reason, stacktrace)
padded_blame = pad(FunctionClauseError.blame(blamed, &inspect/1, &blame_match/1))
[formatted, padded_blame]
_ ->
Exception.format_banner(kind, blamed, stacktrace)
end
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
end
@doc """
Function invoked across nodes for `--rpc-eval`.
"""
def rpc_eval(expr) do
wrapper(fn -> Code.eval_string(expr) end)
catch
kind, reason -> {kind, reason, __STACKTRACE__}
end
## Helpers
defp at_exit(res) do
@@ -131,23 +76,22 @@ defmodule Kernel.CLI do
fun.(elem(res, 1))
catch
:exit, {:shutdown, int} when is_integer(int) ->
send(parent, {self(), {:shutdown, int}})
send parent, {self, {:shutdown, int}}
exit({:shutdown, int})
:exit, reason
when reason == :normal
when reason == :shutdown
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown ->
send(parent, {self(), {:shutdown, 0}})
when reason == :normal
when reason == :shutdown
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown ->
send parent, {self, {:shutdown, 0}}
exit(reason)
kind, reason ->
print_error(kind, reason, __STACKTRACE__)
send(parent, {self(), {:shutdown, 1}})
exit(to_exit(kind, reason, __STACKTRACE__))
stack = System.stacktrace
print_error(kind, reason, stack)
send parent, {self, {:shutdown, 1}}
exit(to_exit(kind, reason, stack))
else
_ ->
send(parent, {self(), res})
send parent, {self, res}
end
end)
@@ -155,7 +99,6 @@ defmodule Kernel.CLI do
{^pid, res} ->
:erlang.demonitor(ref, [:flush])
res
{:DOWN, ^ref, _, _, other} ->
print_error({:EXIT, pid}, other, [])
{:shutdown, 1}
@@ -171,39 +114,18 @@ defmodule Kernel.CLI do
{[h | hs], _} when h == hd(list) ->
new_config = %{config | errors: ["#{h} : Unknown option" | config.errors]}
callback.(hs, new_config)
{new_list, new_config} ->
callback.(new_list, new_config)
end
end
## Error handling
defp print_error(kind, reason, stacktrace) do
IO.write(:stderr, format_error(kind, reason, stacktrace))
defp print_error(kind, reason, trace) do
IO.puts :stderr, Exception.format(kind, reason, prune_stacktrace(trace))
end
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
defp blame_match(%{match?: false, node: node}), do: blame_ansi(:red, "-", node)
defp blame_ansi(color, no_ansi, node) do
if IO.ANSI.enabled?() do
[color | Macro.to_string(node)]
|> IO.ANSI.format(true)
|> IO.iodata_to_binary()
else
no_ansi <> Macro.to_string(node) <> no_ansi
end
end
defp pad(string) do
" " <> String.replace(string, "\n", "\n ")
end
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
:elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
:elixir_def, :elixir_map]
defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
prune_stacktrace(t)
@@ -223,92 +145,65 @@ defmodule Kernel.CLI do
# Parse shared options
defp halt_standalone(opt) do
IO.puts(:stderr, "#{opt} : Standalone options can't be combined with other options")
System.halt(1)
end
defp parse_shared([opt | _], _config) when opt in @standalone_opts do
halt_standalone(opt)
end
defp parse_shared([opt | t], _config) when opt in ["-v", "--version"] do
if function_exported?(IEx, :started?, 0) and IEx.started?() do
IO.puts("IEx " <> System.build_info()[:build])
defp parse_shared([opt | _t], _config) when opt in ["-v", "--version"] do
if function_exported?(IEx, :started?, 0) and IEx.started? do
IO.puts "IEx " <> System.build_info[:build]
else
IO.puts(:erlang.system_info(:system_version))
IO.puts("Elixir " <> System.build_info()[:build])
IO.puts :erlang.system_info(:system_version)
IO.puts "Elixir " <> System.build_info[:build]
end
if t != [] do
halt_standalone(opt)
else
System.halt(0)
end
System.halt 0
end
defp parse_shared(["-pa", h | t], config) do
paths = expand_code_path(h)
Enum.each(paths, &:code.add_patha/1)
parse_shared(t, %{config | pa: config.pa ++ paths})
parse_shared t, %{config | pa: config.pa ++ paths}
end
defp parse_shared(["-pz", h | t], config) do
paths = expand_code_path(h)
Enum.each(paths, &:code.add_pathz/1)
parse_shared(t, %{config | pz: config.pz ++ paths})
parse_shared t, %{config | pz: config.pz ++ paths}
end
defp parse_shared(["--app", h | t], config) do
parse_shared(t, %{config | commands: [{:app, h} | config.commands]})
parse_shared t, %{config | commands: [{:app, h} | config.commands]}
end
defp parse_shared(["--no-halt" | t], config) do
parse_shared(t, %{config | no_halt: true})
parse_shared t, %{config | halt: false}
end
defp parse_shared(["-e", h | t], config) do
parse_shared(t, %{config | commands: [{:eval, h} | config.commands]})
end
defp parse_shared(["--eval", h | t], config) do
parse_shared(t, %{config | commands: [{:eval, h} | config.commands]})
end
defp parse_shared(["--rpc-eval", node, h | t], config) do
node = append_hostname(node)
parse_shared(t, %{config | commands: [{:rpc_eval, node, h} | config.commands]})
end
defp parse_shared(["--rpc-eval" | _], config) do
new_config = %{config | errors: ["--rpc-eval : wrong number of arguments" | config.errors]}
{[], new_config}
parse_shared t, %{config | commands: [{:eval, h} | config.commands]}
end
defp parse_shared(["-r", h | t], config) do
parse_shared(t, %{config | commands: [{:require, h} | config.commands]})
parse_shared t, %{config | commands: [{:require, h} | config.commands]}
end
defp parse_shared(["-pr", h | t], config) do
parse_shared(t, %{config | commands: [{:parallel_require, h} | config.commands]})
parse_shared t, %{config | commands: [{:parallel_require, h} | config.commands]}
end
defp parse_shared([erl, _ | t], config) when erl in ["--erl", "--sname", "--name", "--cookie", "--logger-otp-reports", "--logger-sasl-reports"] do
parse_shared t, config
end
defp parse_shared([erl | t], config) when erl in ["--detached", "--hidden", "--werl"] do
parse_shared t, config
end
defp parse_shared(list, config) do
{list, config}
end
defp append_hostname(node) do
case :string.find(node, "@") do
:nomatch -> node <> :string.find(Atom.to_string(node()), "@")
_ -> node
end
end
defp expand_code_path(path) do
path = Path.expand(path)
case Path.wildcard(path) do
[] -> [to_charlist(path)]
[] -> [to_charlist(path)]
list -> Enum.map(list, &to_charlist/1)
end
end
@@ -320,11 +215,11 @@ defmodule Kernel.CLI do
end
defp parse_argv(["+elixirc" | t], config) do
parse_compiler(t, config)
parse_compiler t, config
end
defp parse_argv(["+iex" | t], config) do
parse_iex(t, %{config | pry: true})
parse_iex t, config
end
defp parse_argv(["-S", h | t], config) do
@@ -334,10 +229,9 @@ defmodule Kernel.CLI do
defp parse_argv([h | t] = list, config) do
case h do
"-" <> _ ->
shared_option?(list, config, &parse_argv(&1, &2))
shared_option? list, config, &parse_argv(&1, &2)
_ ->
if List.keymember?(config.commands, :eval, 0) do
if Keyword.has_key?(config.commands, :eval) do
{config, list}
else
{%{config | commands: [{:file, h} | config.commands]}, t}
@@ -356,46 +250,36 @@ defmodule Kernel.CLI do
end
defp parse_compiler(["-o", h | t], config) do
parse_compiler(t, %{config | output: h})
parse_compiler t, %{config | output: h}
end
defp parse_compiler(["--no-docs" | t], config) do
parse_compiler(t, %{config | compiler_options: [{:docs, false} | config.compiler_options]})
parse_compiler t, %{config | compiler_options: [{:docs, false} | config.compiler_options]}
end
defp parse_compiler(["--no-debug-info" | t], config) do
compiler_options = [{:debug_info, false} | config.compiler_options]
parse_compiler(t, %{config | compiler_options: compiler_options})
parse_compiler t, %{config | compiler_options: [{:debug_info, false} | config.compiler_options]}
end
defp parse_compiler(["--ignore-module-conflict" | t], config) do
compiler_options = [{:ignore_module_conflict, true} | config.compiler_options]
parse_compiler(t, %{config | compiler_options: compiler_options})
parse_compiler t, %{config | compiler_options: [{:ignore_module_conflict, true} | config.compiler_options]}
end
defp parse_compiler(["--warnings-as-errors" | t], config) do
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
parse_compiler(t, %{config | compiler_options: compiler_options})
parse_compiler t, %{config | compiler_options: [{:warnings_as_errors, true} | config.compiler_options]}
end
defp parse_compiler(["--verbose" | t], config) do
parse_compiler(t, %{config | verbose_compile: true})
end
# Private compiler options
defp parse_compiler(["--profile", "time" | t], config) do
parse_compiler(t, %{config | profile: :time})
parse_compiler t, %{config | verbose_compile: true}
end
defp parse_compiler([h | t] = list, config) do
case h do
"-" <> _ ->
shared_option?(list, config, &parse_compiler(&1, &2))
shared_option? list, config, &parse_compiler(&1, &2)
_ ->
pattern = if File.dir?(h), do: "#{h}/**/*.ex", else: h
parse_compiler(t, %{config | compile: [pattern | config.compile]})
parse_compiler t, %{config | compile: [pattern | config.compile]}
end
end
@@ -409,20 +293,26 @@ defmodule Kernel.CLI do
{config, t}
end
# This clause is here so that Kernel.CLI does not
# error out with "unknown option"
defp parse_iex(["--dot-iex", _ | t], config) do
parse_iex t, config
end
defp parse_iex([opt, _ | t], config) when opt in ["--remsh"] do
parse_iex t, config
end
defp parse_iex(["-S", h | t], config) do
{%{config | commands: [{:script, h} | config.commands]}, t}
end
# These clauses are here so that Kernel.CLI does not error out with "unknown option"
defp parse_iex(["--dot-iex", _ | t], config), do: parse_iex(t, config)
defp parse_iex(["--remsh", _ | t], config), do: parse_iex(t, config)
defp parse_iex(["--no-pry" | t], config), do: parse_iex(t, %{config | pry: false})
defp parse_iex([h | t] = list, config) do
case h do
"-" <> _ -> shared_option?(list, config, &parse_iex(&1, &2))
_ -> {%{config | commands: [{:file, h} | config.commands]}, t}
"-" <> _ ->
shared_option? list, config, &parse_iex(&1, &2)
_ ->
{%{config | commands: [{:file, h} | config.commands]}, t}
end
end
@@ -433,32 +323,22 @@ defmodule Kernel.CLI do
# Process commands
defp process_command({:cookie, h}, _config) do
if Node.alive?() do
wrapper(fn -> Node.set_cookie(String.to_atom(h)) end)
if Node.alive? do
wrapper fn -> Node.set_cookie(String.to_atom(h)) end
else
{:error, "--cookie : Cannot set cookie if the node is not alive (set --name or --sname)"}
end
end
defp process_command({:eval, expr}, _config) when is_binary(expr) do
wrapper(fn -> Code.eval_string(expr, []) end)
end
defp process_command({:rpc_eval, node, expr}, _config) when is_binary(expr) do
case :rpc.call(String.to_atom(node), __MODULE__, :rpc_eval, [expr]) do
:ok -> :ok
{:badrpc, {:EXIT, exit}} -> Process.exit(self(), exit)
{:badrpc, reason} -> {:error, "--rpc-eval : RPC failed with reason #{inspect(reason)}"}
{kind, error, stack} -> :erlang.raise(kind, error, stack)
end
wrapper fn -> Code.eval_string(expr, []) end
end
defp process_command({:app, app}, _config) when is_binary(app) do
case Application.ensure_all_started(String.to_atom(app)) do
{:error, {app, reason}} ->
msg = "--app : Could not start application #{app}: " <> Application.format_error(reason)
{:error, msg}
{:error, "--app : Could not start application #{app}: " <>
Application.format_error(reason)}
{:ok, _} ->
:ok
end
@@ -466,7 +346,7 @@ defmodule Kernel.CLI do
defp process_command({:script, file}, _config) when is_binary(file) do
if exec = find_elixir_executable(file) do
wrapper(fn -> Code.require_file(exec) end)
wrapper fn -> Code.require_file(exec) end
else
{:error, "-S : Could not find executable #{file}"}
end
@@ -474,7 +354,7 @@ defmodule Kernel.CLI do
defp process_command({:file, file}, _config) when is_binary(file) do
if File.regular?(file) do
wrapper(fn -> Code.require_file(file) end)
wrapper fn -> Code.require_file(file) end
else
{:error, "No file named #{file}"}
end
@@ -484,7 +364,7 @@ defmodule Kernel.CLI do
files = filter_patterns(pattern)
if files != [] do
wrapper(fn -> Enum.map(files, &Code.require_file(&1)) end)
wrapper fn -> Enum.map files, &Code.require_file(&1) end
else
{:error, "-r : No files matched pattern #{pattern}"}
end
@@ -494,12 +374,7 @@ defmodule Kernel.CLI do
files = filter_patterns(pattern)
if files != [] do
wrapper(fn ->
case Kernel.ParallelCompiler.require(files) do
{:ok, _, _} -> :ok
{:error, _, _} -> exit({:shutdown, 1})
end
end)
wrapper fn -> Kernel.ParallelRequire.files(files) end
else
{:error, "-pr : No files matched pattern #{pattern}"}
end
@@ -512,36 +387,17 @@ defmodule Kernel.CLI do
case filter_multiple_patterns(patterns) do
{:ok, []} ->
{:error, "No files matched provided patterns"}
{:ok, files} ->
wrapper(fn ->
wrapper fn ->
Code.compiler_options(config.compiler_options)
verbose_opts =
opts =
if config.verbose_compile do
[each_file: &IO.puts("Compiling #{Path.relative_to_cwd(&1)}")]
else
[
each_long_compilation:
&IO.puts("Compiling #{Path.relative_to_cwd(&1)} (it's taking more than 10s)")
]
end
profile_opts =
if config.profile do
[profile: config.profile]
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 5s)")]
else
[]
end
opts = verbose_opts ++ profile_opts
case Kernel.ParallelCompiler.compile_to_path(files, config.output, opts) do
{:ok, _, _} -> :ok
{:error, _, _} -> exit({:shutdown, 1})
end
end)
Kernel.ParallelCompiler.files_to_path(files, config.output, opts)
end
{:missing, missing} ->
{:error, "No files matched pattern(s) #{Enum.join(missing, ",")}"}
end
@@ -549,24 +405,31 @@ defmodule Kernel.CLI do
defp filter_patterns(pattern) do
pattern
|> Path.expand()
|> Path.wildcard()
|> :lists.usort()
|> Path.wildcard
|> :lists.usort
|> Enum.filter(&File.regular?/1)
end
defp filter_multiple_patterns(patterns) do
{files, missing} =
Enum.reduce(patterns, {[], []}, fn pattern, {files, missing} ->
case filter_patterns(pattern) do
[] -> {files, [pattern | missing]}
match -> {match ++ files, missing}
end
end)
matched_files = Enum.map patterns, fn(pattern) ->
case filter_patterns(pattern) do
[] -> {:missing, pattern}
files -> {:ok, files}
end
end
case missing do
[] -> {:ok, :lists.usort(files)}
_ -> {:missing, :lists.usort(missing)}
files = Enum.filter_map matched_files,
fn(match) -> elem(match, 0) == :ok end,
&elem(&1, 1)
missing_patterns = Enum.filter_map matched_files,
fn(match) -> elem(match, 0) == :missing end,
&elem(&1, 1)
if missing_patterns == [] do
{:ok, :lists.usort(Enum.concat(files))}
else
{:missing, :lists.usort(missing_patterns)}
end
end
@@ -584,7 +447,6 @@ defmodule Kernel.CLI do
{:win32, _} ->
base = Path.rootname(exec)
if File.regular?(base), do: base, else: exec
_ ->
exec
end
+11 -17
View File
@@ -3,19 +3,16 @@
defmodule Kernel.ErrorHandler do
@moduledoc false
@spec undefined_function(module, atom, list) :: term
def undefined_function(module, fun, args) do
ensure_loaded(module) or ensure_compiled(module, :module, :raise)
ensure_loaded(module) or ensure_compiled(module, :module)
:error_handler.undefined_function(module, fun, args)
end
@spec undefined_lambda(module, fun, list) :: term
def undefined_lambda(module, fun, args) do
ensure_loaded(module) or ensure_compiled(module, :module, :raise)
ensure_loaded(module) or ensure_compiled(module, :module)
:error_handler.undefined_lambda(module, fun, args)
end
@spec ensure_loaded(module) :: boolean
def ensure_loaded(module) do
case :code.ensure_loaded(module) do
{:module, _} -> true
@@ -23,21 +20,18 @@ defmodule Kernel.ErrorHandler do
end
end
@spec ensure_compiled(module, atom, atom) :: :found | :not_found | :deadlock
# Never wait on nil because it should never be defined.
def ensure_compiled(nil, _kind, _deadlock) do
:not_found
def ensure_compiled(nil, _kind) do
false
end
def ensure_compiled(module, kind, deadlock) do
{compiler_pid, file_pid} = :erlang.get(:elixir_compiler_info)
ref = :erlang.make_ref()
modules = :elixir_module.compiler_modules()
send(compiler_pid, {:waiting, kind, self(), ref, file_pid, module, modules, deadlock})
:erlang.garbage_collect(self())
def ensure_compiled(module, kind) do
parent = :erlang.get(:elixir_compiler_pid)
ref = :erlang.make_ref
send parent, {:waiting, kind, self(), ref, module, :elixir_module.compiler_modules()}
:erlang.garbage_collect(self)
receive do
{^ref, value} -> value
{^ref, :found} -> true
{^ref, :not_found} -> false
end
end
end
+138 -156
View File
@@ -1,229 +1,172 @@
# This is an Elixir module responsible for tracking references
# to modules, remote dispatches, and the usage of
# aliases/imports/requires in the Elixir scope.
# This is an Elixir module responsible for tracking
# the usage of aliases, imports and requires in the Elixir scope.
#
# Note that since this is required for bootstrap, we can't use
# any of the `GenServer.Behaviour` conveniences.
defmodule Kernel.LexicalTracker do
@moduledoc false
@timeout :infinity
@timeout 30_000
@behaviour :gen_server
@doc """
Returns all references in this lexical scope.
Returns all remotes referenced in this lexical scope.
"""
def references(pid) do
:gen_server.call(pid, :references, @timeout)
def remote_references(arg) do
:gen_server.call(to_pid(arg), :remote_references, @timeout)
end
@doc """
Returns all remote dispatches in this lexical scope.
"""
def remote_dispatches(arg) do
:gen_server.call(to_pid(arg), :remote_dispatches, @timeout)
end
@doc """
Gets the destination the lexical scope is meant to
compile to.
"""
def dest(arg) do
:gen_server.call(to_pid(arg), :dest, @timeout)
end
defp to_pid(pid) when is_pid(pid), do: pid
defp to_pid(mod) when is_atom(mod) do
table = :elixir_module.data_table(mod)
[{_, val}] = :ets.lookup(table, {:elixir, :lexical_tracker})
val
end
# Internal API
# Starts the tracker and returns its PID.
# Starts the tracker and returns its pid.
@doc false
def start_link() do
:gen_server.start_link(__MODULE__, :ok, [])
def start_link(dest) do
:gen_server.start_link(__MODULE__, dest, [])
end
@doc false
def stop(pid) do
:gen_server.call(pid, :stop)
:gen_server.cast(pid, :stop)
end
@doc false
def add_export(pid, module) when is_atom(module) do
:gen_server.cast(pid, {:add_export, module})
end
@doc false
def add_import(pid, module, fas, line, warn) when is_atom(module) do
def add_import(pid, module, fas, line, warn) do
:gen_server.cast(pid, {:add_import, module, fas, line, warn})
end
@doc false
def add_alias(pid, module, line, warn) when is_atom(module) do
def add_alias(pid, module, line, warn) do
:gen_server.cast(pid, {:add_alias, module, line, warn})
end
@doc false
def remote_dispatch(pid, module, mode) when is_atom(module) do
:gen_server.cast(pid, {:remote_dispatch, module, mode})
def remote_reference(pid, module, mode) do
:gen_server.cast(pid, {:remote_reference, module, mode})
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_dispatch(pid, module, fa, line, mode) do
:gen_server.cast(pid, {:remote_dispatch, module, fa, line, mode})
end
@doc false
def alias_dispatch(pid, module) when is_atom(module) do
def import_dispatch(pid, module, fa, line, mode) do
:gen_server.cast(pid, {:import_dispatch, module, fa, line, mode})
end
@doc false
def alias_dispatch(pid, module) do
:gen_server.cast(pid, {:alias_dispatch, module})
end
@doc false
def import_quoted(pid, module, function, arities) when is_atom(module) do
:gen_server.cast(pid, {:import_quoted, module, function, arities})
end
@doc false
def add_compile_env(pid, app, path, return) do
:gen_server.cast(pid, {:compile_env, app, path, return})
end
@doc false
def set_file(pid, file) do
:gen_server.cast(pid, {:set_file, file})
end
@doc false
def reset_file(pid) do
:gen_server.cast(pid, :reset_file)
end
@doc false
def write_cache(pid, value) do
key = :erlang.unique_integer()
:gen_server.cast(pid, {:write_cache, key, value})
key
end
@doc false
def read_cache(pid, key) do
:gen_server.call(pid, {:read_cache, key}, @timeout)
end
@doc false
def collect_unused_imports(pid) do
unused(pid, :unused_imports)
unused(pid, :import)
end
@doc false
def collect_unused_aliases(pid) do
unused(pid, :unused_aliases)
unused(pid, :alias)
end
defp unused(pid, tag) do
:gen_server.call(pid, tag, @timeout)
:gen_server.call(pid, {:unused, tag}, @timeout)
end
# Callbacks
def init(:ok) do
state = %{
aliases: %{},
imports: %{},
references: %{},
exports: %{},
cache: %{},
compile_env: :ordsets.new(),
file: nil
}
{:ok, state}
def init(dest) do
{:ok, %{directives: %{}, references: %{}, compile: %{},
runtime: %{}, dest: dest}}
end
@doc false
def handle_call(:unused_aliases, _from, state) do
{:reply, Enum.sort(state.aliases), state}
def handle_call({:unused, tag}, _from, state) do
directives =
for {{^tag, module_or_mfa}, marker} <- state.directives,
is_integer(marker),
do: {module_or_mfa, marker}
{:reply, Enum.sort(directives), state}
end
def handle_call(:unused_imports, _from, state) do
{:reply, Enum.sort(state.imports), state}
def handle_call(:remote_references, _from, state) do
{:reply, partition(Enum.to_list(state.references), [], []), state}
end
def handle_call(:references, _from, state) do
{compile, runtime} = partition(Map.to_list(state.references), [], [])
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
def handle_call(:remote_dispatches, _from, state) do
{:reply, {state.compile, state.runtime}, state}
end
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
{:reply, Map.get(cache, key), state}
def handle_call(:dest, _from, state) do
{:reply, state.dest, state}
end
def handle_call(:stop, _from, state) do
{:stop, :normal, :ok, state}
def handle_cast({:remote_reference, module, mode}, state) do
{:noreply, %{state | references: add_reference(state.references, module, mode)}}
end
def handle_cast({:write_cache, key, value}, %{cache: cache} = state) do
{:noreply, %{state | cache: Map.put(cache, key, value)}}
end
def handle_cast({:remote_dispatch, module, mode}, state) do
def handle_cast({:remote_dispatch, module, fa, line, mode}, state) do
references = add_reference(state.references, module, mode)
state = add_remote_dispatch(state, module, fa, line, mode)
{:noreply, %{state | references: references}}
end
def handle_cast({:import_dispatch, module, {function, arity}, mode}, state) do
%{imports: imports, references: references} = state
def handle_cast({:import_dispatch, module, {function, arity} = fa, line, mode}, state) do
state =
state
|> add_import_dispatch(module, function, arity)
|> add_remote_dispatch(module, fa, line, mode)
imports =
case imports do
%{^module => modules_and_fas} ->
modules_and_fas
|> Map.delete(module)
|> Map.delete({function, arity})
|> then(&Map.put(imports, module, &1))
%{} ->
imports
end
references = add_reference(references, module, mode)
{:noreply, %{state | imports: imports, references: references}}
{:noreply, state}
end
def handle_cast({:alias_dispatch, module}, %{aliases: aliases} = state) do
{:noreply, %{state | aliases: Map.delete(aliases, module)}}
def handle_cast({:alias_dispatch, module}, state) do
{:noreply, %{state | directives: add_dispatch(state.directives, module, :alias)}}
end
def handle_cast({:import_quoted, module, function, arities}, state) do
%{imports: imports} = state
def handle_cast({:add_import, module, fas, line, warn}, state) when is_atom(module) do
directives =
state.directives
|> Enum.reject(&match?({{:import, {^module, _, _}}, _}, &1))
|> :maps.from_list
|> add_directive(module, line, warn, :import)
imports =
case imports do
%{^module => modules_and_fas} ->
arities
|> Enum.reduce(modules_and_fas, &Map.delete(&2, {function, &1}))
|> Map.delete(module)
|> then(&Map.put(imports, module, &1))
directives =
Enum.reduce(fas, directives, fn {function, arity}, directives ->
add_directive(directives, {module, function, arity}, line, warn, :import)
end)
%{} ->
imports
end
{:noreply, %{state | imports: imports}}
end
def handle_cast({:set_file, file}, state) do
{:noreply, %{state | file: file}}
end
def handle_cast(:reset_file, state) do
{:noreply, %{state | file: nil}}
end
def handle_cast({:compile_env, app, path, return}, state) do
{:noreply, update_in(state.compile_env, &:ordsets.add_element({app, path, return}, &1))}
end
def handle_cast({:add_export, module}, state) do
{:noreply, put_in(state.exports[module], true)}
end
def handle_cast({:add_import, module, fas, line, warn}, state) do
if warn do
imports = for module_or_fa <- [module | fas], do: {module_or_fa, line}, into: %{}
{:noreply, put_in(state.imports[module], imports)}
else
{:noreply, state}
end
{:noreply, %{state | directives: directives}}
end
def handle_cast({:add_alias, module, line, warn}, state) do
if warn do
{:noreply, put_in(state.aliases[module], line)}
else
{:noreply, state}
end
{:noreply, %{state | directives: add_directive(state.directives, module, line, warn, :alias)}}
end
def handle_cast(:stop, state) do
{:stop, :normal, state}
end
@doc false
@@ -243,21 +186,60 @@ defmodule Kernel.LexicalTracker do
defp partition([{remote, :compile} | t], compile, runtime),
do: partition(t, [remote | compile], runtime)
defp partition([{remote, :runtime} | t], compile, runtime),
do: partition(t, compile, [remote | runtime])
defp partition([], compile, runtime), do: {compile, runtime}
defp partition([], compile, runtime),
do: {compile, runtime}
# Callbacks helpers
defp add_reference(references, module, :runtime) when is_atom(module),
do: map_put_new(module, :runtime, references)
defp add_reference(references, module, :compile) when is_atom(module),
do: Map.put(references, module, :compile)
do: :maps.put(module, :compile, references)
defp add_reference(references, module, :runtime) when is_atom(module) do
case references do
%{^module => _} -> references
%{} -> Map.put(references, module, :runtime)
defp add_remote_dispatch(state, module, fa, line, mode) when is_atom(module) do
map_update mode, %{module => %{fa => [line]}}, state, fn mode_dispatches ->
map_update module, %{fa => [line]}, mode_dispatches, fn module_dispatches ->
map_update fa, [line], module_dispatches, &[line | List.delete(&1, line)]
end
end
end
defp add_import_dispatch(state, module, function, arity) do
directives =
add_dispatch(state.directives, module, :import)
|> add_dispatch({module, function, arity}, :import)
# 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
# In the map we keep imports and aliases.
# If the value is a line, it was imported/aliased and has a pending warning
# If the value is true, it was imported/aliased and used
defp add_directive(directives, module_or_mfa, line, warn, tag) do
marker = if warn, do: line, else: true
:maps.put({tag, module_or_mfa}, marker, directives)
end
defp add_dispatch(directives, module_or_mfa, tag) do
:maps.put({tag, module_or_mfa}, true, directives)
end
defp map_update(key, initial, map, fun) do
case :maps.find(key, map) do
{:ok, val} -> :maps.put(key, fun.(val), map)
:error -> :maps.put(key, initial, map)
end
end
defp map_put_new(key, value, map) do
case :maps.find(key, map) do
{:ok, _} -> map
:error -> :maps.put(key, value, map)
end
end
end
File diff suppressed because it is too large Load Diff
+98 -7
View File
@@ -1,17 +1,108 @@
defmodule Kernel.ParallelRequire do
@moduledoc false
@moduledoc """
A module responsible for requiring files in parallel.
"""
@deprecated "Use Kernel.ParallelCompiler.require/2 instead"
@doc """
Requires the given files.
A callback that will be invoked with each file, or a keyword list of `callbacks` can be provided:
* `:each_file` - invoked with each file
* `:each_module` - invoked with file, module name, and binary code
Returns the modules generated by each required file.
"""
def files(files, callbacks \\ [])
def files(files, callback) when is_function(callback, 1) do
files(files, each_file: callback)
files(files, [each_file: callback])
end
def files(files, options) when is_list(options) do
case Kernel.ParallelCompiler.require(files, options) do
{:ok, modules, _} -> modules
{:error, _, _} -> exit({:shutdown, 1})
def files(files, callbacks) when is_list(callbacks) do
compiler_pid = self()
:elixir_code_server.cast({:reset_warnings, compiler_pid})
schedulers = max(:erlang.system_info(:schedulers_online), 2)
result = spawn_requires(files, [], callbacks, schedulers, [])
# In case --warning-as-errors is enabled and there was a warning,
# compilation status will be set to error.
case :elixir_code_server.call({:compilation_status, compiler_pid}) do
:ok ->
result
:error ->
IO.puts :stderr, "\nExecution failed due to warnings while using the --warnings-as-errors option"
exit({:shutdown, 1})
end
end
defp spawn_requires([], [], _callbacks, _schedulers, result), do: result
defp spawn_requires([], waiting, callbacks, schedulers, result) do
wait_for_messages([], waiting, callbacks, schedulers, result)
end
defp spawn_requires(files, waiting, callbacks, schedulers, result) when length(waiting) >= schedulers do
wait_for_messages(files, waiting, callbacks, schedulers, result)
end
defp spawn_requires([h | t], waiting, callbacks, schedulers, result) do
parent = self()
{pid, ref} = :erlang.spawn_monitor fn ->
:erlang.put(:elixir_compiler_pid, parent)
exit(try do
new = Code.require_file(h) || []
{:required, Enum.map(new, &elem(&1, 0)), h}
catch
kind, reason ->
{:failure, kind, reason, System.stacktrace}
end)
end
spawn_requires(t, [{pid, ref} | waiting], callbacks, schedulers, result)
end
defp wait_for_messages(files, waiting, callbacks, schedulers, result) do
receive do
{:DOWN, ref, :process, pid, status} ->
tuple = {pid, ref}
if tuple in waiting do
waiting = List.delete(waiting, tuple)
case status do
{:required, mods, file} ->
if each_file_callback = callbacks[:each_file] do
each_file_callback.(file)
end
spawn_requires(files, waiting, callbacks, schedulers, mods ++ result)
{:failure, kind, reason, stacktrace} ->
:erlang.raise(kind, reason, stacktrace)
other ->
:erlang.raise(:exit, other, [])
end
else
spawn_requires(files, waiting, callbacks, schedulers, result)
end
{:module_available, child, ref, file, module, binary} ->
if each_module_callback = callbacks[:each_module] do
each_module_callback.(file, module, binary)
end
send(child, {ref, :ack})
spawn_requires(files, waiting, callbacks, schedulers, result)
{:struct_available, _} ->
spawn_requires(files, waiting, callbacks, schedulers, result)
{:waiting, _, child, ref, _, _} ->
send(child, {ref, :not_found})
spawn_requires(files, waiting, callbacks, schedulers, result)
end
end
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -361
View File
@@ -3,16 +3,8 @@ import Kernel, except: [destructure: 2, defdelegate: 2, defstruct: 2]
defmodule Kernel.Utils do
@moduledoc false
@doc """
Callback for destructure.
"""
def destructure(list, count)
when is_list(list) and is_integer(count) and count >= 0,
do: destructure_list(list, count)
def destructure(nil, count)
when is_integer(count) and count >= 0,
do: destructure_nil(count)
def destructure(list, count) when is_list(list), do: destructure_list(list, count)
def destructure(nil, count), do: destructure_nil(count)
defp destructure_list(_, 0), do: []
defp destructure_list([], count), do: destructure_nil(count)
@@ -21,393 +13,82 @@ defmodule Kernel.Utils do
defp destructure_nil(0), do: []
defp destructure_nil(count), do: [nil | destructure_nil(count - 1)]
@doc """
Callback for defdelegate entry point.
"""
def defdelegate_all(funs, opts, env) do
to = Keyword.get(opts, :to) || raise ArgumentError, "expected to: to be given as argument"
as = Keyword.get(opts, :as)
if to == env.module and is_nil(as) do
raise ArgumentError,
"defdelegate function is calling itself, which will lead to an infinite loop. You should either change the value of the :to option or specify the :as option"
end
if is_list(funs) do
IO.warn(
"passing a list to Kernel.defdelegate/2 is deprecated, please define each delegate separately",
Macro.Env.stacktrace(env)
)
end
if Keyword.has_key?(opts, :append_first) do
IO.warn(
"Kernel.defdelegate/2 :append_first option is deprecated",
Macro.Env.stacktrace(env)
)
end
to
end
@doc """
Callback for each function in defdelegate.
"""
def defdelegate_each(fun, opts) when is_list(opts) do
# TODO: Remove on v2.0
append_first? = Keyword.get(opts, :append_first, false)
def defdelegate(fun, opts) do
append_first = Keyword.get(opts, :append_first, false)
{name, args} =
case fun do
{:when, _, [_left, right]} ->
raise ArgumentError,
"guards are not allowed in defdelegate/2, got: when #{Macro.to_string(right)}"
_ ->
case Macro.decompose_call(fun) do
{_, _} = pair -> pair
_ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}"
end
case Macro.decompose_call(fun) do
{_, _} = pair -> pair
_ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}"
end
as_args_list = normalize_args(args)
as = Keyword.get(opts, :as, name)
as_args = build_as_args(args, append_first?)
{name, args, as, as_args}
:lists.map(fn as_args ->
formal_args = make_formal_args(as_args)
as_args = case append_first do
true -> tl(as_args) ++ [hd(as_args)]
false -> as_args
end
{name, formal_args, as, as_args}
end, as_args_list)
end
defp build_as_args(args, append_first?) do
as_args = :lists.map(&build_as_arg/1, args)
case append_first? do
true -> tl(as_args) ++ [hd(as_args)]
false -> as_args
end
defp make_formal_args(args) do
fun = &match?({name, _, mod} when is_atom(name) and is_atom(mod), &1)
:lists.filter(fun, args)
end
defp build_as_arg({:\\, _, [arg, _default_arg]}), do: validate_arg(arg)
defp build_as_arg(arg), do: validate_arg(arg)
defp validate_arg({name, _, mod} = arg) when is_atom(name) and is_atom(mod) do
arg
defp normalize_args(raw_args) do
:lists.foldr(fn
({:\\, _, [arg, default_arg]}, [as_args | _] = as_args_list) ->
new_as_args = [default_arg | as_args]
[new_as_args | add_arg(as_args_list, arg)]
(arg, as_args_list) ->
add_arg(as_args_list, arg)
end, [[]], raw_args)
end
defp validate_arg(ast) do
defp add_arg(as_args_list, {name, _, mod} = arg) when is_atom(name) and is_atom(mod),
do: :lists.map(&([arg | &1]), as_args_list)
defp add_arg(_, code) do
raise ArgumentError,
"defdelegate/2 only accepts function parameters, got: #{Macro.to_string(ast)}"
"defdelegate/2 only accepts function parameters, got: #{Macro.to_string(code)}"
end
@doc """
Callback for defstruct.
"""
def defstruct(module, fields, bootstrapped?) do
{set, bag} = :elixir_module.data_tables(module)
if :ets.member(set, :__struct__) do
raise ArgumentError,
"defstruct has already been called for " <>
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
end
def defstruct(module, fields) do
case fields do
fs when is_list(fs) ->
:ok
other ->
raise ArgumentError, "struct fields definition must be list, got: #{inspect(other)}"
raise ArgumentError, "struct fields definition must be list, got: #{inspect other}"
end
mapper = fn
fields = :lists.map(fn
{key, val} when is_atom(key) ->
try do
:elixir_quote.escape(val, false, :none)
Macro.escape(val)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
else
_ -> {key, val}
end
key when is_atom(key) ->
{key, nil}
other ->
raise ArgumentError, "struct field names must be atoms, got: #{inspect(other)}"
end
raise ArgumentError, "struct field names must be atoms, got: #{inspect other}"
end, fields)
fields = :lists.map(mapper, fields)
enforce_keys =
case :ets.lookup(set, :enforce_keys) do
[{_, enforce_keys, _, _}] when is_list(enforce_keys) ->
:ets.update_element(set, :enforce_keys, {3, :used})
enforce_keys
[{_, enforce_key, _, _}] ->
:ets.update_element(set, :enforce_keys, {3, :used})
[enforce_key]
[] ->
[]
end
# TODO: Make it raise on v2.0
warn_on_duplicate_struct_key(:lists.keysort(1, fields))
foreach = fn
key when is_atom(key) ->
:ok
key ->
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
body =
case bootstrapped? do
true ->
case enforce_keys do
[] ->
quote do
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
%{map | key => val}
end)
end
_ ->
quote do
{map, keys} =
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
{key, val}, {map, keys} ->
{%{map | key => val}, List.delete(keys, key)}
end)
case keys do
[] ->
map
_ ->
raise ArgumentError,
"the following keys must also be given when building " <>
"struct #{inspect(__MODULE__)}: #{inspect(keys)}"
end
end
end
false ->
quote do
:lists.foldl(
fn {key, val}, acc -> %{acc | key => val} end,
@__struct__,
kv
)
end
end
case enforce_keys -- :maps.keys(struct) do
[] ->
# The __struct__ field is used for expansion and for loading remote structs
:ets.insert(set, {:__struct__, struct, nil, []})
# Store all field metadata to go into __info__(:struct)
mapper = fn {key, val} ->
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), quote(do: kv), body}
error_keys ->
raise ArgumentError,
"@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
"#{inspect(fields)}"
end
{:maps.put(:__struct__, module, :maps.from_list(fields)),
List.wrap(Module.get_attribute(module, :enforce_keys)),
Module.get_attribute(module, :derive)}
end
defp warn_on_duplicate_struct_key([]) do
:ok
end
defp warn_on_duplicate_struct_key([{key, _} | [{key, _} | _] = rest]) do
IO.warn("duplicate key #{inspect(key)} found in struct")
warn_on_duplicate_struct_key(rest)
end
defp warn_on_duplicate_struct_key([_ | rest]) do
warn_on_duplicate_struct_key(rest)
end
@doc """
Announcing callback for defstruct.
"""
def announce_struct(module) do
case :erlang.get(:elixir_compiler_info) do
case :erlang.get(:elixir_compiler_pid) do
:undefined -> :ok
{pid, _} -> send(pid, {:available, :struct, module})
end
end
@doc """
Callback for raise.
"""
def raise(msg) when is_binary(msg) do
RuntimeError.exception(msg)
end
def raise(module) when is_atom(module) do
module.exception([])
end
def raise(%_{__exception__: true} = exception) do
exception
end
def raise(other) do
ArgumentError.exception(
"raise/1 and reraise/2 expect a module name, string or exception " <>
"as the first argument, got: #{inspect(other)}"
)
end
@doc """
Callback for defguard.
Rewrites an expression so it can be used both inside and outside a guard.
Take, for example, the expression:
is_integer(value) and rem(value, 2) == 0
If we wanted to create a macro, `is_even`, from this expression, that could be
used in guards, we'd have to take several things into account.
First, if this expression is being used inside a guard, `value` needs to be
unquoted each place it occurs, since it has not yet been at that point in our
macro.
Secondly, if the expression is being used outside of a guard, we want to unquote
`value`, but only once, and then re-use the unquoted form throughout the expression.
This helper does exactly that: takes the AST for an expression and a list of
variable references it should be aware of, and rewrites it into a new expression
that checks for its presence in a guard, then unquotes the variable references as
appropriate.
The following code
expression = quote do: is_integer(value) and rem(value, 2) == 0
variable_references = [value: Elixir]
Kernel.Utils.defguard(expression, variable_references) |> Macro.to_string() |> IO.puts()
would print a code similar to:
case Macro.Env.in_guard?(__CALLER__) do
true ->
quote do
is_integer(unquote(value)) and rem(unquote(value), 2) == 0
end
false ->
quote do
value = unquote(value)
is_integer(value) and rem(value, 2) == 0
end
end
"""
defmacro defguard(args, expr) do
defguard(args, expr, __CALLER__)
end
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
def defguard(args, expr, env) do
{^args, vars} = extract_refs_from_args(args)
env = :elixir_env.with_vars(%{env | context: :guard}, vars)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
quote do
case Macro.Env.in_guard?(__CALLER__) do
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
end
end
end
defp extract_refs_from_args(args) do
Macro.postwalk(args, [], fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
{var, [{ref, var_context(meta, context)} | acc]}
node, acc ->
{node, acc}
end)
end
# Finds every reference to `refs` in `guard` and wraps them in an unquote.
defp unquote_every_ref(guard, refs) do
Macro.postwalk(guard, fn
{ref, meta, context} = var when is_atom(ref) and is_atom(context) ->
case {ref, var_context(meta, context)} in refs do
true -> literal_unquote(var)
false -> var
end
node ->
node
end)
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, module) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
pair = {ref, var_context(meta, context)}
case pair in refs do
true ->
case acc do
%{^pair => {new_var, _}} ->
{new_var, acc}
%{} ->
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
new_var = Macro.unique_var(generated, module)
{new_var, Map.put(acc, pair, {new_var, var})}
end
false ->
{var, acc}
end
node, acc ->
{node, acc}
end)
all_used = for ref <- :lists.reverse(refs), used = :maps.get(ref, used_refs, nil), do: used
{vars, exprs} = :lists.unzip(all_used)
quote do
{unquote_splicing(vars)} = {unquote_splicing(Enum.map(exprs, &literal_unquote/1))}
unquote(guard)
end
end
defp literal_quote(ast) do
{:quote, [], [[do: ast]]}
end
defp literal_unquote(ast) do
{:unquote, [], List.wrap(ast)}
end
defp var_context(meta, kind) do
case :lists.keyfind(:counter, 1, meta) do
{:counter, counter} -> counter
false -> kind
pid -> send(pid, {:struct_available, module})
end
end
end

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