Compare commits

...
78 Commits
Author SHA1 Message Date
José Valim 117b2bf614 Release v1.5.0 2017-07-25 09:13:55 +02:00
José Valim a20acc460b Check for null bytes and invalid environment vars on System 2017-07-21 19:00:11 +02:00
José Valim e5208421b3 Release v1.5.0-rc.2 2017-07-20 11:35:30 +02:00
José Valim d5467a90a7 Ensure recursive file/path ops raise on paths with null bytes 2017-07-20 11:13:52 +02:00
José Valim fa574e65bd Do not use named ETS tables during module definition
Tables named after modules are very likely to be used
the application itself, which can lead to conflicts in
scenarios such as code reloading.

Closes #6361
2017-07-19 23:22:42 +02:00
Andrea Leopardi 8e99e3c74d Bring the "e in _" syntax back to try/rescue (#6360) 2017-07-19 19:14:31 +02:00
José Valim 1515182348 Also deprecate nil which was silently accepted 2017-07-18 13:15:06 +02:00
BobbPrice 686038f040 Handle -compile module attribute when specified as a list (#6356) 2017-07-18 13:15:05 +02:00
José Valim 127a4a4d8a Update CHANGELOG.md 2017-07-18 10:39:06 +02:00
José Valim 6868af0cd6 Do not rely on ops code stacktraces, closes #6352 2017-07-18 10:34:42 +02:00
Wojtek Mach fd8a129b15 Add note about precompiled source code for IEx.Helper.open/2 (#6349) 2017-07-18 10:34:35 +02:00
José Valim 73f76aed88 Allow __FILE__ and __LINE__ customization in EDITOR 2017-07-17 20:43:22 +02:00
Lau Taarnskov ae8262ae5b Add more Time.compare/2 examples
Showing :eq and a an example of dates not having any effect.
2017-07-16 17:24:12 +02:00
Lau Taarnskov 64cd202237 Allow more than Time structs in Time.diff/2 2017-07-16 17:23:59 +02:00
Michał Muskała baf7b7a93b Optimise binary pattern matching in String.Break (#6340)
In downcase and upcase pattern match tail as ::bits instead of ::binary. This
saves us a check on each iteration if the tail is a proper binary. We only do
this check in the very beginning.

In trim_leading additionally make sure every head of the recursive function
pattern matches on a binary in first argument so the single binary match context
optimisation can be applied.
2017-07-16 17:23:56 +02:00
Jaime Cabot aee5dc280b Fix protocols diff calculation between compilations (#6339) 2017-07-16 17:23:52 +02:00
Wojtek Mach 4092c83e9b Remove remaining mentions of rata die (#6336) 2017-07-15 13:00:17 +02:00
Ben Wilson 7e41005ba2 Introduce Registry.unregister_match/4 (#6326) 2017-07-14 18:32:43 +02:00
José Valim 288e8ea69e Move away from rata die reference and use iso_days instead (#6328)
We chose to use the year zero in the astronomical year
numbering as a reference point, as it is also the reference
used by Calendar.ISO, the default calendar used by Elixir,

It is also the same epoch used by Erlang's :calendar module.
Note we chose to not call it gregorian epoch due to the confusion
between January 1 AC 1 and 0000-01-01. The latter actually maps
to January 1 BC 1.

We also moved away from Calendar.Julian, which could have been
based on Calendrical Calculations, and chose Calendar.Holocene,
which adds exactly 10,000 years to Calendar.ISO and as such has
a very straigh-forward implementation.

More information:

  + https://en.wikipedia.org/wiki/Year_zero
  + https://en.wikipedia.org/wiki/Holocene_calendar
2017-07-14 14:48:27 +02:00
José Valim 36069cbcde Ensure Registry.match/4 works with :_ as key 2017-07-14 01:18:12 +02:00
José Valim 5f735de4ca Do not pass kind to definition_scope 2017-07-13 15:47:03 +02:00
José Valim 96ba93481b Continue raising on unbound __CALLER__ 2017-07-13 15:28:24 +02:00
José Valim ea8eff362b Do not silently prune defmacrop 2017-07-13 14:07:02 +02:00
José Valim 879d67eceb Improve coverage and fix regression on Stream.chunk/4 2017-07-12 18:48:28 +02:00
José Valim 704fb9599b Release v1.5.0-rc.1 2017-07-12 14:43:32 +02:00
José Valim c49a598635 Ensure with is tail call optimizable
Closes #6251
2017-07-11 20:31:25 +02:00
José Valim 86f4e3ba15 Do not lose source compile info on protocol consolidation, closes #6270 2017-07-11 20:31:24 +02:00
Ciarán Walsh 50aba8f223 Improvements to IEx.break! docs 2017-07-10 17:30:26 +02:00
José Valim 06b46e71d1 Update IEx helper names 2017-07-10 16:36:01 +02:00
José Valim 26772f7a1d Rename system_info to runtime_info 2017-07-10 16:35:03 +02:00
José Valim c6238e0bc7 Rename e/1 to exports/1 in IEx.Helpers listing 2017-07-10 15:19:43 +02:00
José Valim 2a5eca9d12 Rename e to exports 2017-07-10 15:05:28 +02:00
José Valim f292a072b0 Update CHANGELOG 2017-07-10 15:00:03 +02:00
Michał Muskała cce4db17d3 Improve documentation for Mix.env/0,1 (#6308) 2017-07-10 14:43:29 +02:00
Tan Jay Jun 53985088d5 Improve the failing guards guide (#6312) 2017-07-10 14:43:28 +02:00
Wojtek Mach 413d8334d4 Update docs & typespecs for calendar modules (#6313) 2017-07-10 14:42:57 +02:00
José Valim 35fae47904 Add breakpoint support and user conveniences to IEx 2017-07-10 14:42:40 +02:00
José Valim cf8507bdbb Clean up IEx.Introspection 2017-07-07 20:20:44 +02:00
José Valim 15735b67b7 Refactor IEx.{Server,Evaluator,Pry} APIs and test suites 2017-07-07 16:18:32 +02:00
José Valim b4c73b2a9e Move match vars handling and warnings to elixir_expand
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-07 16:18:27 +02:00
José Valim 4fa496ca4b Ensure defaults and defoverridable work together, closes #6300
This commit changes the private super implementation to have
a tuple with the {kind, name} as first argument.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-06 15:48:06 +02:00
José Valim bcd1ca606d Fix system_info test
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-06 12:07:58 +02:00
José Valim 2199d0ca54 Add IEx.Helpers.system_info/0
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-06 11:14:39 +02:00
José Valim a794c6e952 Ensure run --no-start still triggers loadpaths, closes #6303
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-05 17:01:59 +02:00
José Valim 17ae84ef42 Do not color map's => differently in inspect 2017-07-05 16:20:56 +02:00
José Valim 28505a7233 Update CHANGELOG 2017-07-04 21:06:18 +02:00
José Valim 4113dbc834 chunk/4 -> chunk_every/4 and chunk_by/4 -> chunk_while/4
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 21:03:56 +02:00
José Valim 07167ea75e Share chunking logic between stream and enum
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 21:03:53 +02:00
José Valim 29496930fd Improve docs for chunk_by/4
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 19:08:13 +02:00
José Valim 99712761c5 Update CHANGELOG
Also closes #6298.
2017-07-04 13:03:09 +02:00
José Valim 67db771086 Do not warn on unused variables in rescue, closes #6287
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 13:00:52 +02:00
José Valim f94477b2ef Update CHANGELOG 2017-07-04 11:28:49 +02:00
Andrea Leopardi 75f6af7316 Mention BadMapError in Map.equal?/2 in the CHANGELOG
[ci skip]

Closes #6288.
2017-07-04 11:23:39 +02:00
Eric Meadows-Jönsson 91465a9a42 Add back assert comment in Base test
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:14:24 +02:00
Wojtek Mach 281d749618 Fix DateTime.convert/2 (#6277)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:14:19 +02:00
Eric Meadows-Jönsson 39da6e3980 Fix failing Base test
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:14:13 +02:00
Wojtek Mach bfaca6776a Implement Inspect for DateTime (#6269)
* Implement Inspect for DateTime

* Only use nice inspect for DateTime in ISO

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:14:04 +02:00
Brian Alexander d865aaae73 StringIO.get_until bug fix, performance, and tests (#6268)
* StringIO.get_until bug fix, performance, and tests

* Fix bug in StringIO.get_until result handling

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:13:59 +02:00
James Fish 39bce23c24 Optimise Base encode/decode (#6261)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:13:53 +02:00
Michał Muskała 0deff8b14e Raise Protocol.UndefinedError on bad functions in Enumerable implementation (#6292)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-04 11:13:52 +02:00
José Valim 0391b12263 Raise on more invalid patterns and expressions in guards 2017-07-01 20:08:41 +02:00
José Valim 85badf2a90 Strip docs from escript beams as well
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-01 12:39:30 +02:00
Michał Muskała a8d217e225 Fix binaries optimisation from #6240 (#6272)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-07-01 11:17:51 +02:00
José Valim 473352f7f2 Always return defaults on configuration() access
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-29 14:13:57 +02:00
José Valim b37529e0eb Load ExUnit configuration as late as possible
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-29 13:37:26 +02:00
José Valim fc40868ca4 ETS table names are not quotable on OTP 20
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-29 12:50:31 +02:00
José Valim 9c844fc550 Cache the AST on definitions
This speeds up the compilation time from 10% to 15%
measured across different projects.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-29 11:05:57 +02:00
José Valim 91113f53f2 Do not use .Test prefix in docs
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-28 14:55:33 +02:00
José Valim 6487698c26 Return {:error, :invalid_time} for wrong precision
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-28 10:07:38 +02:00
José Valim 27f256e68f Do not use colors when inspecting for error messages
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-28 10:07:35 +02:00
Ben Cates c01454f3ee Update map.ex (#6267)
Clarify documentation on Map.update/4.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-27 08:09:34 +02:00
Chris Duranti bb69c7ac7c Add exunit doc about trace mode and test timeouts (#6266)
Timeouts get disabled in trace mode.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-27 08:09:32 +02:00
Wojtek Mach 1dc84830a9 Add EEx deprecation to Deprecations.md (#6265)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-26 22:31:12 +02:00
Myron Marston b5056ca29e Mention IEx autocompletion enhancements (#6264)
- Variable name autocompletion was added in #5504.
- Autocompletion of imported `:only` functions was added in #5518.
2017-06-26 22:21:45 +02:00
Wojtek Mach ad4edaffc3 Update CHANGELOG [ci skip] (#6258) 2017-06-25 18:49:11 +02:00
johnwahba 5787447546 Fix stream cycle over empty enumerable (#6253)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-06-25 13:23:02 +02:00
José Valim fa0de77862 Release v1.5.0-rc.0 2017-06-25 13:10:12 +02:00
José Valim 93d098dcba Prepare for release 2017-06-25 13:08:32 +02:00
98 changed files with 5058 additions and 2726 deletions
+65 -6
View File
@@ -18,6 +18,28 @@ Elixir follows the recommendations in [Unicode Annex #31](http://unicode.org/rep
For a complete reference on Elixir syntax, see the [Syntax Reference](https://hexdocs.pm/elixir/syntax-reference.html). For technical details on Unicode support, see [Unicode Syntax](https://hexdocs.pm/elixir/unicode-syntax.html).
## IEx improvements
IEx got many improvements. The autocompletion system is now capable of autocompleting variables and user imports. New helpers have also been added:
* `exports/1` lists all exports (functions and macros) in a given module
* `open/1` opens up the source of a module or function directly in your editor. For example, `open MyApp.Module`
* `runtime_info/0` prints general information about the running system, such as number of cores, runtime version, allocation of memory in the VM and more
IEx also features a breakpoint system for code debugging. The following functions have been added to aid debugging:
* `break!/2` - sets up a breakpoint for a given `Mod.fun/arity`
* `break!/4` - sets up a breakpoint for the given module, function, arity
* `breaks/0` - prints all breakpoints and their ids
* `continue/0` - continues until the next breakpoint in the same process
* `open/0` - opens editor on the current breakpoint
* `remove_breaks/0` - removes all breakpoints in all modules
* `remove_breaks/1` - removes all breakpoints in a given module
* `reset_break/1` - sets the number of stops on the given id to zero
* `reset_break/3` - sets the number of stops on the given module, function, arity to zer
* `respawn/0` - starts a new shell (breakpoints will ask for permission once more)
* `whereami/1` - shows the current location
## Exception.blame
`Exception.blame/3` is a new function in Elixir that is capable of attaching debug information to certain exceptions. Currently this is used to augment `FunctionClauseError`s with a summary of all clauses and which parts of clause match and which ones didn't. For example:
@@ -134,38 +156,48 @@ Overall, using `@impl` has the following advantages:
This release brings further improvements to Calendar types. It adds arithmetic and others functions to `Time`, `Date`, `NaiveDateTime` and `Datetime` as well as conversion between different calendars.
## v1.5.0-dev
## v1.5.0 (2017-07-25)
### 1. Enhancements
#### Elixir
* [Access] Optimize Access.get/2
* [Calendar] Limit `Calendar.ISO` up to year 10000
* [Calendar] Add Rata Die format for conversions between Calendars and `Date.convert/2`, `Time.convert/2`, `NaiveDateTime.convert/2` and `DateTime.convert/2` (as well as bang variants)
* [Access] Optimize `Access.get/2`
* [Base] Optimise Base encode/decode
* [Calendar] Implement Inspect for DateTime with Calendar.ISO
* [Calendar] Add "ISO days" format for conversions between Calendars and `Date.convert/2`, `Time.convert/2`, `NaiveDateTime.convert/2` and `DateTime.convert/2` (as well as bang variants)
* [Calendar] Add `:calendar` field to `Time` struct
* [Calendar] Add `Time.diff/3`, `Date.add/2`, `Date.diff/2`, `DateTime.diff/3`
* [Calendar] Add `Date.range/2`
* [Calendar] Add `Date.new/4`, `DateTime.utc_now/1`, `NaiveDateTime.new/8` and `Time.new/5` that allow specifing calendar
* [Enum] Add `Enum.chunk_by/4` and `Stream.chunk_by/4`
* [Enum] Add `Enum.chunk_every/2` and `Enum.chunk_every/4` with a more explicit API than `Enum.chunk/2` and `Enum.chunk/4`
* [Exception] Add `Exception.blame/3` that adds metadata to exceptions
* [File] Add `File.read_link/1` and `File.read_link!/1`
* [File] Introduce `:trim_bom` option for `File.stream!/2`
* [Inspect] Add `:printable_limit` to control the limit of printable structures
* [Integer] Add `Integer.gcd/2`
* [Kernel] Add `left not in right` to check that the left side is not in the enumerable on the right
* [Kernel] Use the new `debug_info` chunk in OTP 20. This provides a mechanism for tools to retrieve the Elixir AST from beam files
* [Kernel] `defoverridable/1` accepts a module name as argument and marks all callbacks as overridable
* [Kernel] Allow non-quoted Unicode atoms and variables according to Unicode Annex #31 (see Unicode Syntax document)
* [Kernel] Warn when a `:__struct__` key is used when building/updating structs
* [Kernel] Cache the AST on definitions. This speeds up the compilation time from 10% to 15% measured across different projects
* [Kernel] Improve compiler error message on invalid patterns and guards
* [Keyword] Add `replace/3` and `replace!/3` for replacing an existing key
* [List] `List.starts_with?/2`
* [Macro] Introduce `Macro.generate_arguments/2`
* [Map] Optimize `Map.merge/3` by choosing merging direction
* [Map] Add `replace/3` and `replace!/3` for replacing an existing key
* [Map] Raise `BadMapError` in `Map.equal?/2` when either of the two arguments is not a map
* [MapSet] Reduce `MapSet` size when serialized to approximately half
* [Process] Add `Process.cancel_timer/2`
* [Protocol] Show available implementations on `Protocol.UndefinedError` if the protocol has been consolidated
* [Registry] Support ETS guard conditions in `Registry.match/3`
* [Registry] Support `parallel: true` in `Registry.dispatch/3`
* [Registry] Introduce `Registry.unregister_match/4`
* [Stream] Add `Stream.chunk_every/2` and `Stream.chunk_every/4` with a more explicit API than `Stream.chunk/2` and `Stream.chunk/4`
* [String] Optimise binary pattern matching in `String.split/1` and `String.trim_*/1`
* [Supervisor] Add `Supervisor.init/2` and `Supervisor.child_spec/2`
* [Supervisor] Allow `module` and `{module, arg}` to be given to `Supervisor.start_link/2` and invoke `module.child_spec(arg)` on each argument
* [Task] Support `:on_timeout` in `Task.async_stream` to control how tasks are terminated
@@ -174,15 +206,20 @@ This release brings further improvements to Calendar types. It adds arithmetic a
#### ExUnit
* [ExUnit] Show code snippet from test source file in case of test errors
* [ExUnit] Show the value of variables used in an assertion
* [ExUnit] Use `Exception.blame/3` when formatting test errors
* [ExUnit] Make `assert_raise/2` fail if the underlying exception has a broken `message/1` implementation
* [ExUnit] Add `start_supervised/2` and `stop_supervised/1` to ExUnit. Processes started by this function are automatically shut down when the test exits
#### IEx
* [IEx.Autocomplete] Support autocompletion of variable names
* [IEx.Autocomplete] Support autocompletion of functions imported using `import Mod, only: [...]`
* [IEx.Evaluator] Use `Exception.blame/3` when showing errors in the terminal
* [IEx.Helpers] Add `e/1` IEx helper to list all exports in a module
* [IEx.Helpers] Add `exports/1` IEx helper to list all exports in a module
* [IEx.Helpers] Add `break!/2`, `break!/4`, `breaks/0`, `continue/0`, `open/0`, `remove_breaks/0`, `remove_breaks/1`, `reset_break/1`, `reset_break/3` and `whereami/1` for code debugging
* [IEx.Helpers] No longer emit warnings for IEx commands without parentheses
* [IEx.Helpers] Add `runtime_info/0` for printing runtime system information
* [IEx.Helpers] Add `open/1` to open the source of a given module/function in your editor
* [IEx.Info] Implement `IEx.Info` protocol for calendar types
#### Logger
@@ -196,16 +233,32 @@ This release brings further improvements to Calendar types. It adds arithmetic a
* [mix loadpaths] Ensure `--no-deps-check` do not trigger SCM callbacks (such as `git`)
* [mix local.hex] Add `--if-missing` flag to `local.hex` mix task
* [mix profile.cprof] Add `Mix.Tasks.Profile.Cprof` for count-based profiling
* [mix new] New styling for generated applications
### 2. Bug fixes
#### Elixir
* [Calendar] Ensure `Calendar.ISO` raises a readable error when reaching up the year 10000 restriction
* [Calendar] Return `{:error, :invalid_time}` for wrong precision instead of crashing when parsing ISO dates
* [Enumerable] Raise `Protocol.UndefinedError` on bad functions in Enumerable implementation
* [File] Ensure recursive file operations raise on paths with null bytes (*security issue reported by Griffin Byatt*)
* [File] Support `:ram`/`:raw` files in `File.copy/2`
* [Inspect] Do not use colors when inspecting error messages
* [Kernel] Support guards on anonymous functions of zero arity
* [Kernel] Fix compilation of maps used as maps keys inside matches
* [Kernel] Ensure `do` clause in `with` is tail call optimizable
* [Module] `on_definition/6` callback receives body wrapped in a keyword list, such as `[do: body]`. This solves a bug where it was impossible to distinguish between a bodyless clause and a function that returns `nil`.
* [Path] Ensure recursive path operations raise on paths with null bytes (*security issue reported by Griffin Byatt*)
* [Protocol] Do not lose source compile info on protocol consolidation
* [Record] Properly escape quoted expressions passed to `defrecord`
* [Regex] Fix `inspect/2` for regexes with `/` terminator in them
* [Registry] Ensure `Registry.match/4` works with `:_` as key
* [Stream] Fix stream cycle over empty enumerable
* [String] Consider Unicode non-characters valid according to the specification in `String.valid?/1`
* [StringIO] Fix encoding and performance issues in `StringIO.get_until`
* [System] Raise on paths with null bytes in `System.cmd/2` and in `System.find_executable/1` (*security issue reported by Griffin Byatt*)
* [System] Raise on ill-formed environment variables (*security issue reported by Griffin Byatt*)
#### ExUnit
@@ -221,7 +274,9 @@ This release brings further improvements to Calendar types. It adds arithmetic a
* [mix compile.elixir] Store multiple sources in case of module conflicts. This solves an issue where `_build` would get corrupted when compiling Elixir projects with module conflicts
* [mix compile.erlang] Do not silently discard Erlang compile errors
* [mix compile.erlang] Properly track `-compile` module attribute when specified as a list
* [mix compile.protocols] Ensure protocol implementations do not "disappear" when switching between applications in umbrella projects by having separate consolidation paths per project
* [mix compile.protocols] Do not raise when consolidating a protocol that was converted into a module
### 3. Soft deprecations (no warnings emitted)
@@ -247,6 +302,10 @@ This release brings further improvements to Calendar types. It adds arithmetic a
* [String] `String.lstrip/2` and `String.rstrip/2` are deprecated in favor of `String.trim_leading/2` and `String.trim_trailing/2` with a binary as second argument
* [Typespec] `char_list/0` type is deprecated in favor of `charlist/0`
#### EEx
* [EEx] Deprecate `<%= ` in "middle" and "end" expressions, e.g.: `<%= else %>` and `<%= end %>`
## v1.4
The CHANGELOG for v1.4 releases can be found [in the v1.4 branch](https://github.com/elixir-lang/elixir/blob/v1.4/CHANGELOG.md).
+1 -1
View File
@@ -1,7 +1,7 @@
REBAR ?= "$(CURDIR)/rebar"
PREFIX ?= /usr/local
SHARE_PREFIX ?= $(PREFIX)/share
CANONICAL := master/
CANONICAL := v1.5/
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
ERLC := erlc -I lib/elixir/include
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
+6 -7
View File
@@ -10,18 +10,17 @@ This document simply outlines the release process:
3. Ensure CHANGELOG is updated and add current date
4. If a new `vMAJOR.MINOR`, update in [Deprecations](lib/elixir/pages/Deprecations.md) page the link
to [vVERSION]'s changelog (located at the end of the file) by replacing "master" with "vVERSION"
4. If a new `vMAJOR.MINOR`, replace "master" with "vVERSION" in the "Deprecations" page and commit
5. Commit changes above with title "Release vVERSION" and generate new tag
5. If a new `vMAJOR.MINOR`, create a new branch "vMAJOR.MINOR" and set `CANONICAL=` in Makefile
6. Run `make clean test` to ensure all tests pass from scratch and the CI is green
6. Commit changes above with title "Release vVERSION" and generate new tag
7. Recompile an existing project (for example, Ecto) to ensure manifests can be upgraded
7. Run `make clean test` to ensure all tests pass from scratch and the CI is green
8. Push branch and the new tag
8. Recompile an existing project (for example, Ecto) to ensure manifests can be upgraded
9. If a new `vMAJOR.MINOR`, create a new branch "vMAJOR.MINOR" and set `CANONICAL=` in Makefile before building docs
9. Push branch and the new tag
10. Publish new zips with `make zips`, upload `Precompiled.zip` and `Docs.zip` to GitHub Releases
+1 -1
View File
@@ -1 +1 @@
1.5.0-dev
1.5.0
+389 -248
View File
@@ -92,58 +92,145 @@ defmodule Base do
"""
b16_alphabet = Enum.with_index '0123456789ABCDEF'
b64_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
b64url_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
b32_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
b32hex_alphabet = Enum.with_index '0123456789ABCDEFGHIJKLMNOPQRSTUV'
b16_alphabet = '0123456789ABCDEF'
b64_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
b64url_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
b32_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
b32hex_alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
Enum.each [{:enc16, :dec16, b16_alphabet},
{:enc32, :dec32, b32_alphabet},
{:enc64, :dec64, b64_alphabet},
{:enc64url, :dec64url, b64url_alphabet},
{:enc32hex, :dec32hex, b32hex_alphabet}], fn({enc, dec, alphabet}) ->
for {encoding, value} <- alphabet do
defp unquote(enc)(unquote(value)), do: unquote(encoding)
defp unquote(dec)(unquote(encoding)), do: unquote(value)
end
defp unquote(dec)(c) do
raise ArgumentError, "non-alphabet digit found: #{inspect <<c>>, binaries: :as_strings} (byte #{c})"
defmacrop encode_pair(alphabet, case, value) do
quote do
case unquote(value) do
unquote(encode_pair_clauses(alphabet, case))
end
end
end
@compile {:inline, from_upper: 1, from_lower: 1, from_mixed: 1,
to_lower: 1, to_upper: 1, enc16: 1, dec16: 1,
enc32: 1, dec32: 1, enc32hex: 1, dec32hex: 1,
enc64: 1, dec64: 1, enc64url: 1, dec64url: 1}
defp encode_pair_clauses(alphabet, case) when case in [:sensitive, :upper] do
shift = shift(alphabet)
alphabet
|> Enum.with_index()
|> encode_clauses(shift)
end
defp to_lower(char) when char in ?A..?Z,
do: char + (?a - ?A)
defp to_lower(char),
do: char
defp encode_pair_clauses(alphabet, :lower) do
shift = shift(alphabet)
alphabet
|> Stream.map(fn c -> (if c in ?A..?Z, do: c - ?A + ?a, else: c) end)
|> Enum.with_index()
|> encode_clauses(shift)
end
defp to_upper(char), do: char
defp shift(alphabet) do
alphabet
|> length()
|> :math.log2()
|> round()
end
defp from_upper(char), do: char
defp encode_clauses(alphabet, shift) do
for {encoding1, value1} <- alphabet, {encoding2, value2} <- alphabet do
encoding = bsl(encoding1, 8) + encoding2
value = bsl(value1, shift) + value2
[clause] = quote do: (unquote(value) -> unquote(encoding))
clause
end
end
defp from_lower(char) when char in ?a..?z,
do: char - (?a - ?A)
defp from_lower(char) when char not in ?A..?Z,
do: char
defp from_lower(char),
do: raise(ArgumentError, "non-alphabet digit found: \"#{<<char>>}\" (byte #{char})")
defmacrop decode_char(alphabet, case, encoding) do
quote do
case unquote(encoding) do
unquote(decode_char_clauses(alphabet, case))
end
end
end
defp from_mixed(char) when char in ?a..?z,
do: char - (?a - ?A)
defp from_mixed(char),
do: char
defp decode_char_clauses(alphabet, case) when case in [:sensitive, :upper] do
clauses =
alphabet
|> Enum.with_index()
|> decode_clauses()
clauses ++ bad_digit_clause()
end
defp maybe_pad(subject, false, _, _),
do: subject
defp maybe_pad(subject, _, group_size, pad) do
case rem(byte_size(subject), group_size) do
0 -> subject
x -> subject <> String.duplicate(pad, group_size - x)
defp decode_char_clauses(alphabet, :lower) do
{uppers, rest} =
alphabet
|> Stream.with_index()
|> Enum.split_with(fn {encoding, _} -> encoding in ?A..?Z end)
lowers =
Enum.map(uppers, fn {encoding, value} -> {encoding - ?A + ?a, value} end)
if length(uppers) > length(rest) do
decode_mixed_clauses(lowers, rest)
else
decode_mixed_clauses(rest, lowers)
end
end
defp decode_char_clauses(alphabet, :mixed) when length(alphabet) == 16 do
alphabet = Enum.with_index(alphabet)
lowers =
alphabet
|> Stream.filter(fn {encoding, _} -> encoding in ?A..?Z end)
|> Enum.map(fn {encoding, value} -> {encoding - ?A + ?a, value} end)
decode_mixed_clauses(alphabet, lowers)
end
defp decode_char_clauses(alphabet, :mixed) when length(alphabet) == 32 do
alphabet
|> Stream.with_index()
|> Enum.flat_map(fn {encoding, value} = pair ->
if encoding in ?A..?Z do
[pair, {encoding - ?A + ?a, value}]
else
[pair]
end
end)
|> decode_clauses()
end
defp decode_mixed_clauses(first, second) do
first_clauses = decode_clauses(first)
second_clauses = decode_clauses(second) ++ bad_digit_clause()
join_clause =
quote do
encoding ->
case encoding do
unquote(second_clauses)
end
end
first_clauses ++ join_clause
end
defp decode_clauses(alphabet) do
for {encoding, value} <- alphabet do
[clause] = quote do: (unquote(encoding) -> unquote(value))
clause
end
end
defp bad_digit_clause() do
quote do
c ->
raise ArgumentError,
"non-alphabet digit found: #{inspect <<c>>, binaries: :as_strings} (byte #{c})"
end
end
defp maybe_pad(body, "", _, _),
do: body
defp maybe_pad(body, tail, false, _),
do: body <> tail
defp maybe_pad(body, tail, _, group_size) do
case group_size - rem(byte_size(tail), group_size) do
^group_size -> body <> tail
6 -> body <> tail <> "======"
5 -> body <> tail <> "====="
4 -> body <> tail <> "===="
3 -> body <> tail <> "==="
2 -> body <> tail <> "=="
1 -> body <> tail <> "="
end
end
@@ -587,7 +674,7 @@ defmodule Base do
def hex_encode32(data, opts \\ []) when is_binary(data) do
case = Keyword.get(opts, :case, :upper)
pad? = Keyword.get(opts, :padding, true)
do_hex_encode32(case, data, pad?)
do_encode32hex(case, data, pad?)
end
@doc """
@@ -678,7 +765,7 @@ defmodule Base do
def hex_decode32!(string, opts \\ []) when is_binary(string) do
case = Keyword.get(opts, :case, :upper)
pad? = Keyword.get(opts, :padding, true)
do_hex_decode32(case, string, pad?)
do_decode32hex(case, string, pad?)
end
defp remove_ignored(string, nil), do: string
@@ -686,244 +773,298 @@ defmodule Base do
for <<char::8 <- string>>, char not in '\s\t\r\n', into: <<>>, do: <<char::8>>
end
defp do_encode16(_, <<>>), do: <<>>
defp do_encode16(:upper, data) do
for <<c::4 <- data>>, into: <<>>, do: <<enc16(c)::8>>
enc16 = [upper: :enc16_upper, lower: :enc16_lower]
for {case, fun} <- enc16 do
defp unquote(fun)(char) do
encode_pair(unquote(b16_alphabet), unquote(case), char)
end
end
defp do_encode16(:lower, data) do
for <<c::4 <- data>>, into: <<>>, do: <<to_lower(enc16(c))::8>>
defp do_encode16(_, <<>>), do: <<>>
for {case, fun} <- enc16 do
defp do_encode16(unquote(case), data) do
split = 8 * div(byte_size(data), 8)
<<main::size(split)-binary, rest::binary>> = data
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16, unquote(fun)(c6)::16,
unquote(fun)(c7)::16, unquote(fun)(c8)::16>>
end
case rest do
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16, unquote(fun)(c6)::16,
unquote(fun)(c7)::16>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16, unquote(fun)(c6)::16>>
<<c1::8, c2::8, c3::8, c4::8, c5::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16>>
<<c1::8, c2::8, c3::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16>>
<<c1::8, c2::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16>>
<<c1::8>> ->
<<main::binary, unquote(fun)(c1)::16>>
<<>> ->
main
end
end
end
dec16 = [upper: :dec16_upper, lower: :dec16_lower, mixed: :dec16_mixed]
for {case, fun} <- dec16 do
defp unquote(fun)(encoding) do
decode_char(unquote(b16_alphabet), unquote(case), encoding)
end
end
defp do_decode16(_, <<>>), do: <<>>
defp do_decode16(:upper, string) when rem(byte_size(string), 2) == 0 do
for <<c1::8, c2::8 <- string>>, into: <<>> do
<<dec16(c1)::4, dec16(c2)::4>>
end
end
defp do_decode16(:lower, string) when rem(byte_size(string), 2) == 0 do
for <<c1::8, c2::8 <- string>>, into: <<>> do
<<dec16(from_lower(c1))::4, dec16(from_lower(c2))::4>>
end
end
defp do_decode16(:mixed, string) when rem(byte_size(string), 2) == 0 do
for <<c1::8, c2::8 <- string>>, into: <<>> do
<<dec16(from_mixed(c1))::4, dec16(from_mixed(c2))::4>>
for {case, fun} <- dec16 do
defp do_decode16(unquote(case), string) do
split = 8 * div(byte_size(string), 8)
<<main::size(split)-binary, rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::4, unquote(fun)(c2)::4,
unquote(fun)(c3)::4, unquote(fun)(c4)::4,
unquote(fun)(c5)::4, unquote(fun)(c6)::4,
unquote(fun)(c7)::4, unquote(fun)(c8)::4>>
end
case rest do
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> ->
<<main::bits, unquote(fun)(c1)::4, unquote(fun)(c2)::4,
unquote(fun)(c3)::4, unquote(fun)(c4)::4,
unquote(fun)(c5)::4, unquote(fun)(c6)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<main::bits, unquote(fun)(c1)::4, unquote(fun)(c2)::4,
unquote(fun)(c3)::4, unquote(fun)(c4)::4>>
<<c1::8, c2::8>> ->
<<main::bits, unquote(fun)(c1)::4, unquote(fun)(c2)::4>>
<<_::8>> ->
raise ArgumentError, "odd-length string"
<<>> ->
main
end
end
end
defp do_encode64(<<>>, _), do: <<>>
defp do_encode64(data, pad?) do
split = 3 * div(byte_size(data), 3)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::6 <- main>>, into: <<>>, do: <<enc64(c)::8>>
tail = case rest do
<<c1::6, c2::6, c3::4>> ->
<<enc64(c1)::8, enc64(c2)::8, enc64(bsl(c3, 2))::8>>
<<c1::6, c2::2>> ->
<<enc64(c1)::8, enc64(bsl(c2, 4))::8>>
<<>> ->
<<>>
for {base, alphabet} <- ["64": b64_alphabet, "64url": b64url_alphabet] do
pair = :"enc#{base}_pair"
char = :"enc#{base}_char"
do_encode = :"do_encode#{base}"
defp unquote(pair)(value) do
encode_pair(unquote(alphabet), :sensitive, value)
end
main <> maybe_pad(tail, pad?, 4, "=")
end
defp do_decode64(<<>>, _), do: <<>>
defp do_decode64(string, false) do
maybe_pad(string, true, 4, "=") |> do_decode64(true)
end
defp do_decode64(string, _pad?) when rem(byte_size(string), 4) == 0 do
split = byte_size(string) - 4
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec64(c)::6>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<dec64(c1)::6, bsr(dec64(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<dec64(c1)::6, dec64(c2)::6, bsr(dec64(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<dec64(c1)::6, dec64(c2)::6, dec64(c3)::6, dec64(c4)::6>>
<<>> ->
<<>>
defp unquote(char)(value) do
value
|> unquote(pair)()
|> band(0x00FF)
end
main <> tail
end
defp do_decode64(_, _) do
raise ArgumentError, "incorrect padding"
end
defp do_encode64url(<<>>, _), do: <<>>
defp do_encode64url(data, pad?) do
split = 3 * div(byte_size(data), 3)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::6 <- main>>, into: <<>>, do: <<enc64url(c)::8>>
tail = case rest do
<<c1::6, c2::6, c3::4>> ->
<<enc64url(c1)::8, enc64url(c2)::8, enc64url(bsl(c3, 2))::8>>
<<c1::6, c2::2>> ->
<<enc64url(c1)::8, enc64url(bsl(c2, 4))::8>>
<<>> ->
<<>>
defp unquote(do_encode)(<<>>, _), do: <<>>
defp unquote(do_encode)(data, pad?) do
split = 6 * div(byte_size(data), 6)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c1::12, c2::12, c3::12, c4::12 <- main>>, into: <<>> do
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(c3)::16, unquote(pair)(c4)::16>>
end
tail = case rest do
<<c1::12, c2::12, c3::12, c::4>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16, unquote(pair)(c3)::16,
unquote(char)(bsl(c, 2))::8>>
<<c1::12, c2::12, c3::8>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(bsl(c3, 4))::16>>
<<c1::12, c2::12>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16>>
<<c1::12, c2::4>> ->
<<unquote(pair)(c1)::16, unquote(char)(bsl(c2, 2))::8>>
<<c1::8>> ->
<<unquote(pair)(bsl(c1, 4))::16>>
<<>> ->
<<>>
end
maybe_pad(main, tail, pad?, 4)
end
main <> maybe_pad(tail, pad?, 4, "=")
end
defp do_decode64url(<<>>, _), do: <<>>
defp do_decode64url(string, false) do
maybe_pad(string, true, 4, "=") |> do_decode64url(true)
end
defp do_decode64url(string, _pad?) when rem(byte_size(string), 4) == 0 do
split = byte_size(string) - 4
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec64url(c)::6>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<dec64url(c1)::6, bsr(dec64url(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<dec64url(c1)::6, dec64url(c2)::6, bsr(dec64url(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<dec64url(c1)::6, dec64url(c2)::6, dec64url(c3)::6, dec64url(c4)::6>>
<<>> ->
<<>>
for {base, alphabet} <- ["64": b64_alphabet, "64url": b64url_alphabet] do
fun = :"dec#{base}"
do_decode = :"do_decode#{base}"
defp unquote(fun)(encoding) do
decode_char(unquote(alphabet), :sensitive, encoding)
end
defp unquote(do_decode)(<<>>, _), do: <<>>
defp unquote(do_decode)(string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(segs)-binary-unit(64), rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::6, unquote(fun)(c2)::6, unquote(fun)(c3)::6,
unquote(fun)(c4)::6, unquote(fun)(c5)::6, unquote(fun)(c6)::6,
unquote(fun)(c7)::6, unquote(fun)(c8)::6>>
end
case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, bsr(unquote(fun)(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
bsr(unquote(fun)(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
bsr(unquote(fun)(c6), 4)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
unquote(fun)(c6)::6, bsr(unquote(fun)(c7), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
unquote(fun)(c6)::6, unquote(fun)(c7)::6, unquote(fun)(c8)::6>>
<<c1::8, c2::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, bsr(unquote(fun)(c2), 4)::2>>
<<c1::8, c2::8, c3::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
bsr(unquote(fun)(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
bsr(unquote(fun)(c6), 4)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
unquote(fun)(c6)::6, bsr(unquote(fun)(c7), 2)::4>>
_ ->
raise ArgumentError, "incorrect padding"
end
end
main <> tail
end
defp do_decode64url(_, _) do
raise ArgumentError, "incorrect padding"
end
defp do_encode32(_, <<>>, _), do: <<>>
for {base, alphabet} <- ["32": b32_alphabet, "32hex": b32hex_alphabet],
case <- [:upper, :lower] do
pair = :"enc#{base}_#{case}_pair"
char = :"enc#{base}_#{case}_char"
do_encode = :"do_encode#{base}"
for {case, fun} <- [upper: :to_upper, lower: :to_lower] do
defp do_encode32(unquote(case), data, pad?) do
defp unquote(pair)(value) do
encode_pair(unquote(alphabet), unquote(case), value)
end
defp unquote(char)(value) do
value
|> unquote(pair)()
|> band(0x00FF)
end
defp unquote(do_encode)(_, <<>>, _), do: <<>>
defp unquote(do_encode)(unquote(case), data, pad?) do
split = 5 * div(byte_size(data), 5)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::5 <- main>>, into: <<>>, do: <<unquote(fun)(enc32(c))::8>>
main =
for <<c1::10, c2::10, c3::10, c4::10 <- main>>, into: <<>> do
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(c3)::16, unquote(pair)(c4)::16>>
end
tail = case rest do
<<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(c2))::8,
unquote(fun)(enc32(c3))::8, unquote(fun)(enc32(c4))::8,
unquote(fun)(enc32(c5))::8, unquote(fun)(enc32(c6))::8,
unquote(fun)(enc32(bsl(c7, 3)))::8>>
<<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(c2))::8,
unquote(fun)(enc32(c3))::8, unquote(fun)(enc32(c4))::8,
unquote(fun)(enc32(bsl(c5, 1)))::8>>
<<c1::5, c2::5, c3::5, c4::1>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(c2))::8,
unquote(fun)(enc32(c3))::8, unquote(fun)(enc32(bsl(c4, 4)))::8>>
<<c1::5, c2::3>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(bsl(c2, 2)))::8>>
<<c1::10, c2::10, c3::10, c4::2>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(c3)::16, unquote(char)(bsl(c4, 3))::8>>
<<c1::10, c2::10, c3::4>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(char)(bsl(c3, 1))::8>>
<<c1::10, c2::6>> ->
<<unquote(pair)(c1)::16, unquote(pair)(bsl(c2, 4))::16>>
<<c1::8>> ->
<<unquote(pair)(bsl(c1, 2))::16>>
<<>> ->
<<>>
end
main <> maybe_pad(tail, pad?, 8, "=")
maybe_pad(main, tail, pad?, 8)
end
end
defp do_decode32(_, <<>>, _), do: <<>>
defp do_decode32(case, string, false),
do: do_decode32(case, maybe_pad(string, true, 8, "="), true)
for {base, alphabet} <- ["32": b32_alphabet, "32hex": b32hex_alphabet],
case <- [:upper, :lower, :mixed] do
fun = :"dec#{base}_#{case}"
do_decode = :"do_decode#{base}"
for {case, fun} <- [upper: :from_upper, lower: :from_lower, mixed: :from_mixed] do
defp do_decode32(unquote(case), string, _pad?) when rem(byte_size(string), 8) == 0 do
split = byte_size(string) - 8
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec32(unquote(fun)(c))::5>>
tail = case rest do
defp unquote(fun)(encoding) do
decode_char(unquote(alphabet), unquote(case), encoding)
end
defp unquote(do_decode)(_, <<>>, _), do: <<>>
defp unquote(do_decode)(unquote(case), string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(segs)-binary-unit(64), rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
unquote(fun)(c7)::5, unquote(fun)(c8)::5>>
end
case rest do
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
<<dec32(unquote(fun)(c1))::5, bsr(dec32(unquote(fun)(c2)), 2)::3>>
<<main::bits, unquote(fun)(c1)::5, bsr(unquote(fun)(c2), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, bsr(dec32(unquote(fun)(c4)), 4)::1>>
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, bsr(unquote(fun)(c4), 4)::1>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, dec32(unquote(fun)(c4))::5,
bsr(dec32(unquote(fun)(c5)), 1)::4>>
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
bsr(unquote(fun)(c5), 1)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, dec32(unquote(fun)(c4))::5,
dec32(unquote(fun)(c5))::5, dec32(unquote(fun)(c6))::5,
bsr(dec32(unquote(fun)(c7)), 3)::2>>
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
bsr(unquote(fun)(c7), 3)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, dec32(unquote(fun)(c4))::5,
dec32(unquote(fun)(c5))::5, dec32(unquote(fun)(c6))::5,
dec32(unquote(fun)(c7))::5, dec32(unquote(fun)(c8))::5>>
<<>> ->
<<>>
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
unquote(fun)(c7)::5, unquote(fun)(c8)::5>>
<<c1::8, c2::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, bsr(unquote(fun)(c2), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, bsr(unquote(fun)(c4), 4)::1>>
<<c1::8, c2::8, c3::8, c4::8, c5::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
bsr(unquote(fun)(c5), 1)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
bsr(unquote(fun)(c7), 3)::2>>
_ ->
raise ArgumentError, "incorrect padding"
end
main <> tail
end
end
defp do_decode32(_, _, _),
do: raise ArgumentError, "incorrect padding"
defp do_hex_encode32(_, <<>>, _), do: <<>>
for {case, fun} <- [upper: :to_upper, lower: :to_lower] do
defp do_hex_encode32(unquote(case), data, pad?) do
split = 5 * div(byte_size(data), 5)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::5 <- main>>, into: <<>>, do: <<unquote(fun)(enc32hex(c))::8>>
tail = case rest do
<<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(c2))::8,
unquote(fun)(enc32hex(c3))::8, unquote(fun)(enc32hex(c4))::8,
unquote(fun)(enc32hex(c5))::8, unquote(fun)(enc32hex(c6))::8,
unquote(fun)(enc32hex(bsl(c7, 3)))::8>>
<<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(c2))::8,
unquote(fun)(enc32hex(c3))::8, unquote(fun)(enc32hex(c4))::8,
unquote(fun)(enc32hex(bsl(c5, 1)))::8>>
<<c1::5, c2::5, c3::5, c4::1>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(c2))::8,
unquote(fun)(enc32hex(c3))::8, unquote(fun)(enc32hex(bsl(c4, 4)))::8>>
<<c1::5, c2::3>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(bsl(c2, 2)))::8>>
<<>> ->
<<>>
end
main <> maybe_pad(tail, pad?, 8, "=")
end
end
defp do_hex_decode32(_, <<>>, _), do: <<>>
defp do_hex_decode32(case, string, false),
do: do_hex_decode32(case, maybe_pad(string, true, 8, "="), true)
for {case, fun} <- [upper: :from_upper, lower: :from_lower, mixed: :from_mixed] do
defp do_hex_decode32(unquote(case), string, _pad?) when rem(byte_size(string), 8) == 0 do
split = byte_size(string) - 8
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec32hex(unquote(fun)(c))::5>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, bsr(dec32hex(unquote(fun)(c2)), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, bsr(dec32hex(unquote(fun)(c4)), 4)::1>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, dec32hex(unquote(fun)(c4))::5,
bsr(dec32hex(unquote(fun)(c5)), 1)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, dec32hex(unquote(fun)(c4))::5,
dec32hex(unquote(fun)(c5))::5, dec32hex(unquote(fun)(c6))::5,
bsr(dec32hex(unquote(fun)(c7)), 3)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, dec32hex(unquote(fun)(c4))::5,
dec32hex(unquote(fun)(c5))::5, dec32hex(unquote(fun)(c6))::5,
dec32hex(unquote(fun)(c7))::5, dec32hex(unquote(fun)(c8))::5>>
<<>> ->
<<>>
end
main <> tail
end
end
defp do_hex_decode32(_, _, _),
do: raise ArgumentError, "incorrect padding"
end
+10 -11
View File
@@ -35,17 +35,16 @@ defmodule Calendar do
@typedoc """
The internal date format that is used when converting between calendars.
This is the amount of days including the fractional part that has passed of the last day,
since midnight 1 January AD 1 of the Proleptic Gregorian Calendar
(0000-01-01+00:00T00:00.00000 in ISO 8601 notation).
This is the amount of days including the fractional part that has passed of
the last day since 0000-01-01+00:00T00:00.00000 in ISO 8601 notation (also
known as midnight 1 January BC 1 of the Proleptic Gregorian Calendar).
The `parts_per_day` represent how many subparts the current day is subdivided in
(for different calendars, picking a different `parts_per_day` might make sense).
The `parts_in_day` represents how many of these `parts_per_day` have passed in the last day.
Thus, a Rata Die like `{1234, {1, 2}}` should be read as `1234½`.
The `parts_in_day` represents how many of these `parts_per_day` have passed in the
last day.
"""
@type rata_die :: {days :: integer, day_fraction}
@type iso_days :: {days :: integer, day_fraction}
@typedoc """
Microseconds with stored precision.
@@ -127,14 +126,14 @@ defmodule Calendar do
@callback time_to_string(hour, minute, second, microsecond) :: String.t
@doc """
Converts the given datetime (with time zone) into the `t:rata_die` format.
Converts the given datetime (with time zone) into the `t:iso_days` format.
"""
@callback naive_datetime_to_rata_die(year, month, day, hour, minute, second, microsecond) :: rata_die
@callback naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) :: iso_days
@doc """
Converts `t:rata_die` to the Calendar's datetime format.
Converts `t:iso_days` to the Calendar's datetime format.
"""
@callback naive_datetime_from_rata_die(rata_die) :: {year, month, day, hour, minute, second, microsecond}
@callback naive_datetime_from_iso_days(iso_days) :: {year, month, day, hour, minute, second, microsecond}
@doc """
Converts the given time to the `t:day_fraction` format.
+75 -43
View File
@@ -67,8 +67,11 @@ defmodule Date do
## Examples
iex> Date.range(~D[2000-01-01], ~D[2001-01-01])
#DateRange<~D[2000-01-01], ~D[2001-01-01]>
iex> Date.range(~D[1999-01-01], ~D[2000-01-01])
#DateRange<~D[1999-01-01], ~D[2000-01-01]>
iex> Date.range(~N[2000-01-01 09:00:00], ~D[1999-01-01])
#DateRange<~N[2000-01-01 09:00:00], ~D[1999-01-01]>
A range of dates implements the `Enumerable` protocol, which means
functions in the `Enum` module can be used to work with
@@ -79,24 +82,25 @@ defmodule Date do
366
iex> Enum.member?(range, ~D[2001-02-01])
true
iex> Enum.reduce(range, 0, fn(_date, acc) -> acc - 1 end)
iex> Enum.reduce(range, 0, fn _date, acc -> acc - 1 end)
-366
"""
@spec range(Date.t, Date.t) :: Date.Range.t
@spec range(Calendar.date, Calendar.date) :: Date.Range.t
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
{first_days, _} = to_rata_die(first)
{last_days, _} = to_rata_die(last)
{first_days, _} = to_iso_days(first)
{last_days, _} = to_iso_days(last)
%Date.Range{
first: first,
last: last,
first_rata_die: first_days,
last_rata_die: last_days,
first_in_iso_days: first_days,
last_in_iso_days: last_days,
}
end
def range(%Date{}, %Date{}) do
def range(%{calendar: _, year: _, month: _, day: _},
%{calendar: _, year: _, month: _, day: _}) do
raise ArgumentError, "both dates must have matching calendars"
end
@@ -125,7 +129,7 @@ defmodule Date do
end
@doc """
Returns true if the year in `date` is a leap year.
Returns true if the year in the given `date` is a leap year.
## Examples
@@ -149,7 +153,7 @@ defmodule Date do
end
@doc """
Returns the number of days in the given date month.
Returns the number of days in the given `date` month.
## Examples
@@ -309,7 +313,7 @@ defmodule Date do
end
@doc """
Converts a `Date` struct to an Erlang date tuple.
Converts the given `date` to an Erlang date tuple.
Only supports converting dates which are in the ISO calendar,
or other calendars in which the days also start at midnight.
@@ -375,7 +379,7 @@ defmodule Date do
end
@doc """
Compares two `Date` structs.
Compares two date structs.
Returns `:gt` if first date is later than the second
and `:lt` for vice versa. If the two dates are equal
@@ -408,7 +412,7 @@ defmodule Date do
end
def compare(date1, date2) do
if Calendar.compatible_calendars?(date1.calendar, date2.calendar) do
case {to_rata_die(date1), to_rata_die(date2)} do
case {to_iso_days(date1), to_iso_days(date2)} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
@@ -424,21 +428,33 @@ defmodule Date do
end
@doc """
Converts a date from one calendar to another.
Converts the given `date` from it's calendar to the given `calendar`.
Returns `{:ok, date}` if the calendars are compatible,
or `{:error, :incompatible_calendars}` if they are not.
See also `Calendar.compatible_calendars?/2`.
## 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> Date.convert(~D[2000-01-01], Calendar.Holocene)
{:ok, %Date{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1}}
"""
@spec convert(Calendar.date(), Calendar.calendar) :: {:ok, Date.t} | {:error, :incompatible_calendars}
def convert(%Date{calendar: calendar} = date, calendar), do: {:ok, date}
@spec convert(Calendar.date, Calendar.calendar) :: {:ok, t} | {:error, :incompatible_calendars}
def convert(%{calendar: calendar, year: year, month: month, day: day}, calendar) do
{:ok, %Date{calendar: calendar, year: year, month: month, day: day}}
end
def convert(%{calendar: calendar} = date, target_calendar) do
if Calendar.compatible_calendars?(calendar, target_calendar) do
result_date =
date
|> to_rata_die()
|> from_rata_die(target_calendar)
|> to_iso_days()
|> from_iso_days(target_calendar)
{:ok, result_date}
else
{:error, :incompatible_calendars}
@@ -448,8 +464,18 @@ defmodule Date do
@doc """
Similar to `Date.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> Date.convert!(~D[2000-01-01], Calendar.Holocene)
%Date{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1}
"""
@spec convert!(Date.t, Calendar.calendar) :: Date.t
@spec convert!(Calendar.date, Calendar.calendar) :: t
def convert!(date, calendar) do
case convert(date, calendar) do
{:ok, value} ->
@@ -460,7 +486,7 @@ defmodule Date do
end
@doc """
Adds the number of days to the given date.
Adds the number of days to the given `date`.
The days are counted as gregorian days. The date is returned in the same
calendar as it was given in.
@@ -472,11 +498,14 @@ defmodule Date do
iex> Date.add(~D[2000-01-01], 2)
~D[2000-01-03]
iex> Date.add(~N[2000-01-01 09:00:00], 2)
~D[2000-01-03]
"""
@spec add(Date.t, integer()) :: Date.t
def add(%Date{calendar: calendar} = date, days) do
{rata_days, fraction} = to_rata_die(date)
from_rata_die({rata_days + days, fraction}, calendar)
@spec add(Calendar.date, integer()) :: t
def add(%{calendar: calendar} = date, days) do
{iso_days_days, fraction} = to_iso_days(date)
from_iso_days({iso_days_days + days, fraction}, calendar)
end
@doc """
@@ -493,42 +522,45 @@ defmodule Date do
iex> Date.diff(~D[2000-01-01], ~D[2000-01-03])
-2
iex> Date.diff(~D[2000-01-01], ~N[2000-01-03 09:00:00])
-2
"""
@spec diff(Date.t, Date.t) :: integer
def diff(%Date{calendar: Calendar.ISO, year: year1, month: month1, day: day1},
%Date{calendar: Calendar.ISO, year: year2, month: month2, day: day2}) do
Calendar.ISO.date_to_rata_die_days(year1, month1, day1) -
Calendar.ISO.date_to_rata_die_days(year2, month2, day2)
@spec diff(Calendar.date, Calendar.date) :: integer
def diff(%{calendar: Calendar.ISO, year: year1, month: month1, day: day1},
%{calendar: Calendar.ISO, year: year2, month: month2, day: day2}) do
Calendar.ISO.date_to_iso_days_days(year1, month1, day1) -
Calendar.ISO.date_to_iso_days_days(year2, month2, day2)
end
def diff(%Date{} = date1, %Date{} = date2) do
if Calendar.compatible_calendars?(date1.calendar, date2.calendar) do
{days1, _} = to_rata_die(date1)
{days2, _} = to_rata_die(date2)
def diff(%{calendar: calendar1} = date1, %{calendar: calendar2} = date2) do
if Calendar.compatible_calendars?(calendar1, calendar2) do
{days1, _} = to_iso_days(date1)
{days2, _} = to_iso_days(date2)
days1 - days2
else
raise ArgumentError, "cannot calculate the difference between #{inspect date1} and #{inspect date2} because their calendars are not compatible and thus the result would be ambiguous"
end
end
defp to_rata_die(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_rata_die_days(year, month, day), {0, 86400000000}}
defp to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days_days(year, month, day), {0, 86400000000}}
end
defp to_rata_die(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.naive_datetime_to_rata_die(year, month, day, 0, 0, 0, {0, 0})
defp to_iso_days(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.naive_datetime_to_iso_days(year, month, day, 0, 0, 0, {0, 0})
end
defp from_rata_die({days, _}, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_rata_die_days(days)
defp from_iso_days({days, _}, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_iso_days_days(days)
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
end
defp from_rata_die(rata_die, target_calendar) do
{year, month, day, _, _, _, _} = target_calendar.naive_datetime_from_rata_die(rata_die)
defp from_iso_days(iso_days, target_calendar) do
{year, month, day, _, _, _, _} = target_calendar.naive_datetime_from_iso_days(iso_days)
%Date{year: year, month: month, day: day, calendar: target_calendar}
end
@doc """
Calculates the day of the week of a given `Date` struct.
Calculates the day of the week of a given `date`.
Returns the day of the week as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 7, where
+15 -16
View File
@@ -13,22 +13,21 @@ defmodule Date.Range do
"""
@type t :: %__MODULE__{first: Date.t, last: Date.t,
first_rata_die: rata_die_days, last_rata_die: rata_die_days}
first_in_iso_days: Calendar.days,
last_in_iso_days: Calendar.days}
@opaque rata_die_days :: Calendar.days
defstruct [:first, :last, :first_rata_die, :last_rata_die]
defstruct [:first, :last, :first_in_iso_days, :last_in_iso_days]
defimpl Enumerable do
def member?(%{first: %{calendar: calendar, year: first_year, month: first_month, day: first_day},
last: %{calendar: calendar, year: last_year, month: last_month, day: last_day},
first_rata_die: first_rata_die, last_rata_die: last_rata_die},
first_in_iso_days: first_in_iso_days, last_in_iso_days: last_in_iso_days},
%Date{calendar: calendar, year: year, month: month, day: day}) do
first = {first_year, first_month, first_day}
last = {last_year, last_month, last_day}
date = {year, month, day}
if first_rata_die <= last_rata_die do
if first_in_iso_days <= last_in_iso_days do
{:ok, date >= first and date <= last}
else
{:ok, date >= last and date <= first}
@@ -39,13 +38,13 @@ defmodule Date.Range do
{:ok, false}
end
def count(%Date.Range{first_rata_die: first_rata_die, last_rata_die: last_rata_die}) do
{:ok, abs(first_rata_die - last_rata_die) + 1}
def count(%Date.Range{first_in_iso_days: first_in_iso_days, last_in_iso_days: last_in_iso_days}) do
{:ok, abs(first_in_iso_days - last_in_iso_days) + 1}
end
def reduce(%Date.Range{first_rata_die: first_rata_die, last_rata_die: last_rata_die,
def reduce(%Date.Range{first_in_iso_days: first_in_iso_days, last_in_iso_days: last_in_iso_days,
first: %{calendar: calendar}}, acc, fun) do
reduce(first_rata_die, last_rata_die, acc, fun, calendar, first_rata_die <= last_rata_die)
reduce(first_in_iso_days, last_in_iso_days, acc, fun, calendar, first_in_iso_days <= last_in_iso_days)
end
defp reduce(_x, _y, {:halt, acc}, _fun, _calendar, _up?) do
@@ -57,24 +56,24 @@ defmodule Date.Range do
end
defp reduce(x, y, {:cont, acc}, fun, calendar, up? = true) when x <= y do
reduce(x + 1, y, fun.(date_from_rata_days(x, calendar), acc), fun, calendar, up?)
reduce(x + 1, y, fun.(date_from_iso_days_days(x, calendar), acc), fun, calendar, up?)
end
defp reduce(x, y, {:cont, acc}, fun, calendar, up? = false) when x >= y do
reduce(x - 1, y, fun.(date_from_rata_days(x, calendar), acc), fun, calendar, up?)
reduce(x - 1, y, fun.(date_from_iso_days_days(x, calendar), acc), fun, calendar, up?)
end
defp reduce(_, _, {:cont, acc}, _fun, _calendar, _up) do
{:done, acc}
end
defp date_from_rata_days(days, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_rata_die_days(days)
defp date_from_iso_days_days(days, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_iso_days_days(days)
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
end
defp date_from_rata_days(days, calendar) do
{year, month, day, _, _, _, _} = calendar.naive_datetime_from_rata_die({days, {0, 86400000000}})
defp date_from_iso_days_days(days, calendar) do
{year, month, day, _, _, _, _} = calendar.naive_datetime_from_iso_days({days, {0, 86400000000}})
%Date{year: year, month: month, day: day, calendar: calendar}
end
end
+139 -105
View File
@@ -12,6 +12,11 @@ defmodule DateTime do
are structural and based on the DateTime struct fields. For proper
comparison between datetimes, use the `compare/2` function.
The functions on this module work with the `DateTime` struct as well
as any struct that contains the same fields as the `DateTime` struct.
Such functions expect `t:Calendar.datetime/0` in their typespecs
(instead of `t:t/0`).
Developers should avoid creating the DateTime struct directly
and instead rely on the functions provided by this module as
well as the ones in 3rd party calendar libraries.
@@ -40,7 +45,7 @@ defmodule DateTime do
time_zone: Calendar.time_zone, zone_abbr: Calendar.zone_abbr,
utc_offset: Calendar.utc_offset, std_offset: Calendar.std_offset}
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1}) - 365
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1})
@doc """
Returns the current datetime in UTC.
@@ -52,13 +57,13 @@ defmodule DateTime do
"Etc/UTC"
"""
@spec utc_now(Calendar.calendar) :: DateTime.t
@spec utc_now(Calendar.calendar) :: t
def utc_now(calendar \\ Calendar.ISO) do
System.os_time |> from_unix!(:native, calendar)
end
@doc """
Converts the given Unix time to DateTime.
Converts the given Unix time to `DateTime`.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
@@ -69,27 +74,24 @@ defmodule DateTime do
## Examples
iex> DateTime.from_unix(1464096368)
{:ok, %DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {0, 0}, minute: 26,
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
year: 2016, zone_abbr: "UTC"}}
iex> {:ok, datetime} = DateTime.from_unix(1464096368)
iex> datetime
#DateTime<2016-05-24 13:26:08Z>
iex> DateTime.from_unix(1432560368868569, :microsecond)
{:ok, %DateTime{calendar: Calendar.ISO, day: 25, hour: 13, microsecond: {868569, 6}, minute: 26,
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
year: 2015, zone_abbr: "UTC"}}
iex> {:ok, datetime} = DateTime.from_unix(1432560368868569, :microsecond)
iex> datetime
#DateTime<2015-05-25 13:26:08.868569Z>
The unit can also be an integer as in `t:System.time_unit/0`:
iex> DateTime.from_unix(143256036886856, 1024)
{:ok, %DateTime{calendar: Calendar.ISO, day: 17, hour: 7, microsecond: {320312, 3},
minute: 5, month: 3, second: 22, std_offset: 0, time_zone: "Etc/UTC",
utc_offset: 0, year: 6403, zone_abbr: "UTC"}}
iex> {:ok, datetime} = DateTime.from_unix(143256036886856, 1024)
iex> datetime
#DateTime<6403-03-17 07:05:22.320Z>
Negative Unix times are supported, up to -62167219200 seconds,
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
"""
@spec from_unix(integer, :native | System.time_unit, Calendar.calendar) :: {:ok, DateTime.t} | {:error, atom}
@spec from_unix(integer, :native | System.time_unit, Calendar.calendar) :: {:ok, t} | {:error, atom}
def from_unix(integer, unit \\ :second, calendar \\ Calendar.ISO) when is_integer(integer) do
case Calendar.ISO.from_unix(integer, unit) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} ->
@@ -103,7 +105,7 @@ defmodule DateTime do
end
@doc """
Converts the given Unix time to DateTime.
Converts the given Unix time to `DateTime`.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
@@ -116,22 +118,16 @@ defmodule DateTime do
# An easy way to get the Unix epoch is passing 0 to this function
iex> DateTime.from_unix!(0)
%DateTime{calendar: Calendar.ISO, day: 1, hour: 0, microsecond: {0, 0},
minute: 0, month: 1, second: 0, std_offset: 0, time_zone: "Etc/UTC",
utc_offset: 0, year: 1970, zone_abbr: "UTC"}
#DateTime<1970-01-01 00:00:00Z>
iex> DateTime.from_unix!(1464096368)
%DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {0, 0}, minute: 26,
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
year: 2016, zone_abbr: "UTC"}
#DateTime<2016-05-24 13:26:08Z>
iex> DateTime.from_unix!(1432560368868569, :microsecond)
%DateTime{calendar: Calendar.ISO, day: 25, hour: 13, microsecond: {868569, 6}, minute: 26,
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
year: 2015, zone_abbr: "UTC"}
#DateTime<2015-05-25 13:26:08.868569Z>
"""
@spec from_unix!(integer, :native | System.time_unit, Calendar.calendar) :: DateTime.t
@spec from_unix!(integer, :native | System.time_unit, Calendar.calendar) :: t
def from_unix!(integer, unit \\ :second, calendar \\ Calendar.ISO) when is_atom(unit) do
case from_unix(integer, unit, calendar) do
{:ok, datetime} ->
@@ -142,19 +138,19 @@ defmodule DateTime do
end
@doc """
Converts the given NaiveDateTime to DateTime.
Converts the given `NaiveDateTime` to `DateTime`.
It expects a time zone to put the NaiveDateTime in.
Currently it only supports "Etc/UTC" as time zone.
## Examples
iex> DateTime.from_naive(~N[2016-05-24 13:26:08.003], "Etc/UTC")
{:ok, %DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {3000, 3}, minute: 26,
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
year: 2016, zone_abbr: "UTC"}}
iex> {:ok, datetime} = DateTime.from_naive(~N[2016-05-24 13:26:08.003], "Etc/UTC")
iex> datetime
#DateTime<2016-05-24 13:26:08.003Z>
"""
@spec from_naive(NaiveDateTime.t, Calendar.time_zone) :: {:ok, DateTime.t}
@spec from_naive(NaiveDateTime.t, Calendar.time_zone) :: {:ok, t}
def from_naive(naive_datetime, time_zone)
def from_naive(%NaiveDateTime{calendar: calendar,
@@ -166,7 +162,7 @@ defmodule DateTime do
end
@doc """
Converts the given NaiveDateTime to DateTime.
Converts the given `NaiveDateTime` to `DateTime`.
It expects a time zone to put the NaiveDateTime in.
Currently it only supports "Etc/UTC" as time zone.
@@ -174,12 +170,10 @@ defmodule DateTime do
## Examples
iex> DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC")
%DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {3000, 3}, minute: 26,
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
year: 2016, zone_abbr: "UTC"}
#DateTime<2016-05-24 13:26:08.003Z>
"""
@spec from_naive!(non_neg_integer, :native | System.time_unit) :: DateTime.t
@spec from_naive!(NaiveDateTime.t, Calendar.time_zone) :: t
def from_naive!(naive_datetime, time_zone) do
case from_naive(naive_datetime, time_zone) do
{:ok, datetime} ->
@@ -190,9 +184,9 @@ defmodule DateTime do
end
@doc """
Converts the given DateTime to Unix time.
Converts the given `datetime` to Unix time.
The DateTime is expected to be using the ISO calendar
The `datetime` is expected to be using the ISO calendar
with a year greater than or equal to 0.
It will return the integer with the given unit,
@@ -216,18 +210,18 @@ defmodule DateTime do
-17412508655
"""
@spec to_unix(DateTime.t, System.time_unit) :: non_neg_integer
@spec to_unix(Calendar.datetime, System.time_unit) :: integer
def to_unix(datetime, unit \\ :second)
def to_unix(%DateTime{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
{days, fraction} = to_rata_die(datetime)
unix_units = Calendar.ISO.rata_die_to_unit({days - @unix_days, fraction}, unit)
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
{days, fraction} = to_iso_days(datetime)
unix_units = Calendar.ISO.iso_days_to_unit({days - @unix_days, fraction}, unit)
offset_units = System.convert_time_unit(utc_offset + std_offset, :second, unit)
unix_units - offset_units
end
@doc """
Converts a `DateTime` into a `NaiveDateTime`.
Converts the given `datetime` into a `NaiveDateTime`.
Because `NaiveDateTime` does not hold time zone information,
any time zone related data will be lost during the conversion.
@@ -241,7 +235,8 @@ defmodule DateTime do
~N[2000-02-29 23:00:07.0]
"""
def to_naive(%DateTime{year: year, month: month, day: day, calendar: calendar,
@spec to_naive(t) :: NaiveDateTime.t
def to_naive(%DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
%NaiveDateTime{year: year, month: month, day: day, calendar: calendar,
hour: hour, minute: minute, second: second, microsecond: microsecond}
@@ -262,6 +257,7 @@ defmodule DateTime do
~D[2000-02-29]
"""
@spec to_date(t) :: Date.t
def to_date(%DateTime{year: year, month: month, day: day, calendar: calendar}) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@@ -281,6 +277,7 @@ defmodule DateTime do
~T[23:00:07.0]
"""
@spec to_time(t) :: Time.t
def to_time(%DateTime{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}) do
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}
end
@@ -367,15 +364,17 @@ defmodule DateTime do
## Examples
iex> DateTime.from_iso8601("2015-01-23T23:50:07Z")
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 23, microsecond: {0, 0}, minute: 50, month: 1, second: 7, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 2015, zone_abbr: "UTC"}, 0}
iex> DateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 21, microsecond: {123000, 3}, minute: 20, month: 1, second: 7, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 2015, zone_abbr: "UTC"}, 9000}
iex> DateTime.from_iso8601("2015-01-23T23:50:07,123+02:30")
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 21, microsecond: {123000, 3}, minute: 20, month: 1, second: 7, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 2015, zone_abbr: "UTC"}, 9000}
iex> {:ok, datetime, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z")
iex> datetime
#DateTime<2015-01-23 23:50:07Z>
iex> {:ok, datetime, 9000} = DateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
iex> datetime
#DateTime<2015-01-23 21:20:07.123Z>
iex> {:ok, datetime, 9000} = DateTime.from_iso8601("2015-01-23T23:50:07,123+02:30")
iex> datetime
#DateTime<2015-01-23 21:20:07.123Z>
iex> DateTime.from_iso8601("2015-01-23P23:50:07")
{:error, :invalid_format}
@@ -411,11 +410,12 @@ defmodule DateTime do
{:ok, offset} <- parse_offset(rest) do
%{year: year, month: month, day: day} = date
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
{_, precision} = microsecond
datetime =
Calendar.ISO.naive_datetime_to_rata_die(year, month, day, hour, minute, second, microsecond)
Calendar.ISO.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> apply_tz_offset(offset)
|> from_rata_die("Etc/UTC", "UTC", 0, 0, calendar)
|> from_iso_days("Etc/UTC", "UTC", 0, 0, calendar, precision)
{:ok, %{datetime | microsecond: microsecond}, offset}
else
@@ -437,7 +437,7 @@ defmodule DateTime do
end
@doc """
Converts the given datetime to a string according to its calendar.
Converts the given `datetime` to a string according to its calendar.
### Examples
@@ -479,8 +479,21 @@ defmodule DateTime do
end
end
defimpl Inspect do
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}, _) do
"#DateTime<" <> Calendar.ISO.datetime_to_string(year, month, day, hour, minute, second, microsecond,
time_zone, zone_abbr, utc_offset, std_offset) <> ">"
end
def inspect(datetime, opts) do
Inspect.Any.inspect(datetime, opts)
end
end
@doc """
Compares two `DateTime` structs.
Compares two datetime structs.
Returns `:gt` if first datetime is later than the second
and `:lt` for vice versa. If the two datetimes are equal
@@ -501,24 +514,24 @@ defmodule DateTime do
:gt
"""
@spec compare(DateTime.t, DateTime.t) :: :lt | :eq | :gt
@spec compare(Calendar.datetime, Calendar.datetime) :: :lt | :eq | :gt
def compare(%DateTime{utc_offset: utc_offset1, std_offset: std_offset1} = datetime1,
%DateTime{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2) do
{days1, {parts1, ppd1}} =
datetime1
|> to_rata_die()
|> to_iso_days()
|> apply_tz_offset(utc_offset1 + std_offset1)
{days2, {parts2, ppd2}} =
datetime2
|> to_rata_die()
|> to_iso_days()
|> apply_tz_offset(utc_offset2 + std_offset2)
# Ensure fraction tuples have same denominator.
rata_die1 = {days1, parts1 * ppd2}
rata_die2 = {days2, parts2 * ppd1}
iso_days1 = {days1, parts1 * ppd2}
iso_days2 = {days2, parts2 * ppd1}
case {rata_die1, rata_die2} do
case {iso_days1, iso_days2} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
@@ -547,90 +560,111 @@ defmodule DateTime do
-18000
"""
@spec diff(DateTime.t, DateTime.t) :: integer()
def diff(%DateTime{utc_offset: utc_offset1, std_offset: std_offset1} = datetime1,
%DateTime{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2, unit \\ :second) do
@spec diff(Calendar.datetime, Calendar.datetime) :: integer()
def diff(%{utc_offset: utc_offset1, std_offset: std_offset1} = datetime1,
%{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2, unit \\ :second) do
naive_diff =
(datetime1 |> to_rata_die() |> Calendar.ISO.rata_die_to_unit(unit)) -
(datetime2 |> to_rata_die() |> Calendar.ISO.rata_die_to_unit(unit))
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
offset_diff =
(utc_offset2 + std_offset2) - (utc_offset1 + std_offset1)
naive_diff + System.convert_time_unit(offset_diff, :second, unit)
end
@doc """
Converts a DateTime from one calendar to another.
Converts a given `datetime` from one calendar to another.
If this conversion fails for some reason, an `{:error, reason}` tuple is returned.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an `{:error, :incompatible_calendars}` tuple
is returned.
## 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> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.convert(dt1, Calendar.ISO)
{:ok, %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
hour: 23, minute: 0, second: 7, microsecond: {0, 0},
utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}}
iex> DateTime.convert(dt1, Calendar.Holocene)
{:ok, %DateTime{calendar: Calendar.Holocene, day: 29, hour: 23,
microsecond: {0, 0}, minute: 0, month: 2, second: 7, std_offset: 0,
time_zone: "America/Manaus", utc_offset: -14400, year: 12000,
zone_abbr: "AMT"}}
"""
@spec convert(DateTime.t, Calendar.calendar) :: {:ok, DateTime.t} | {:error, atom}
def convert(%DateTime{calendar: calendar} = datetime, calendar) do
{:ok, datetime}
@spec convert(Calendar.datetime, Calendar.calendar) :: {:ok, t} | {:error, :incompatible_calendars}
def convert(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}, calendar) do
{:ok, %DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}}
end
def convert(%DateTime{} = datetime, calendar) do
result_datetime =
datetime
|> to_rata_die
|> from_rata_die(datetime, calendar)
{:ok, result_datetime}
def convert(%{calendar: dt_calendar, microsecond: {_, precision}} = datetime, calendar) do
if Calendar.compatible_calendars?(dt_calendar, calendar) do
result_datetime =
datetime
|> to_iso_days
|> from_iso_days(datetime, calendar, precision)
{:ok, result_datetime}
else
{:error, :incompatible_calendars}
end
end
@doc """
Converts a `DateTime` struct from one calendar to another.
Converts a given `datetime` from one calendar to another.
If this conversion fails for some reason, an `ArgumentError` is raised.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an ArgumentError is raised.
## 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> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.convert!(dt1, Calendar.ISO)
%DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
hour: 23, minute: 0, second: 7, microsecond: {0, 0},
utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.convert!(dt1, Calendar.Holocene)
%DateTime{calendar: Calendar.Holocene, day: 29, hour: 23,
microsecond: {0, 0}, minute: 0, month: 2, second: 7, std_offset: 0,
time_zone: "America/Manaus", utc_offset: -14400, year: 12000,
zone_abbr: "AMT"}
"""
@spec convert!(DateTime.t, Calendar.calendar) :: DateTime.t
@spec convert!(Calendar.datetime, Calendar.calendar) :: t | no_return
def convert!(datetime, calendar) do
case convert(datetime, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect datetime} to target calendar #{inspect calendar}, reason: #{inspect reason}"
{:error, :incompatible_calendars} ->
raise ArgumentError, "cannot convert #{inspect datetime} to target calendar #{inspect calendar}, reason: #{inspect datetime.calendar} and #{inspect calendar} have different day rollover moments, making this conversion ambiguous"
end
end
defp to_rata_die(%DateTime{calendar: calendar,year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_rata_die(year, month, day, hour, minute, second, microsecond)
defp to_iso_days(%{calendar: calendar,year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
end
defp from_rata_die(rata_die, datetime, calendar) do
defp from_iso_days(iso_days, datetime, calendar, precision) do
%{time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset} = datetime
from_rata_die(rata_die, time_zone, zone_abbr, utc_offset, std_offset, calendar)
from_iso_days(iso_days, time_zone, zone_abbr, utc_offset, std_offset, calendar, precision)
end
defp from_rata_die(rata_die, time_zone, zone_abbr, utc_offset, std_offset, calendar) do
{year, month, day, hour, minute, second, microsecond} = calendar.naive_datetime_from_rata_die(rata_die)
%DateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
defp from_iso_days(iso_days, time_zone, zone_abbr, utc_offset, std_offset, calendar, precision) do
{year, month, day, hour, minute, second, {microsecond, _}} = calendar.naive_datetime_from_iso_days(iso_days)
%DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision},
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}
end
defp apply_tz_offset(rata_die, offset) do
Calendar.ISO.add_day_fraction_to_rata_die(rata_die, -offset, 86400)
defp apply_tz_offset(iso_days, offset) do
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
end
end
+38 -40
View File
@@ -30,46 +30,44 @@ defmodule Calendar.ISO do
@microseconds_per_second 1_000_000
@doc """
Returns the normalized Rata Die representation of the specified date.
Returns the `t:Calendar.iso_days` format of the specified date.
## Examples
iex> Calendar.ISO.naive_datetime_to_rata_die(1, 1, 1, 0, 0, 0, {0, 6})
{1, {0, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_rata_die(2000, 1, 1, 12, 0, 0, {0, 6})
{730120, {43200000000, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_rata_die(2000, 1, 1, 13, 0, 0, {0, 6})
{730120, {46800000000, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(0, 1, 1, 0, 0, 0, {0, 6})
{0, {0, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 12, 0, 0, {0, 6})
{730485, {43200000000, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 13, 0, 0, {0, 6})
{730485, {46800000000, 86400000000}}
"""
@spec naive_datetime_to_rata_die(Calendar.year, Calendar.month, Calendar.day,
@spec naive_datetime_to_iso_days(Calendar.year, Calendar.month, Calendar.day,
Calendar.hour, Calendar.minute, Calendar.second,
Calendar.microsecond) :: Calendar.rata_die
def naive_datetime_to_rata_die(year, month, day, hour, minute, second, microsecond) do
{date_to_rata_die_days(year, month, day),
Calendar.microsecond) :: Calendar.iso_days
def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
{date_to_iso_days_days(year, month, day),
time_to_day_fraction(hour, minute, second, microsecond)}
end
@doc """
Converts a Rata Die to the datetime format specified by this calendar.
Converts the `t:Calendar.iso_days` format to the datetime format specified by this calendar.
## Examples
iex> Calendar.ISO.naive_datetime_from_rata_die({1, {0, 86400}})
{1, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_rata_die({730120, {0, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
{0, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730485, {0, 86400}})
{2000, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_rata_die({730120, {43200, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({730485, {43200, 86400}})
{2000, 1, 1, 12, 0, 0, {0, 6}}
"""
@spec naive_datetime_from_rata_die(Calendar.rata_die) ::
@spec naive_datetime_from_iso_days(Calendar.iso_days) ::
{Calendar.year, Calendar.month, Calendar.day,
Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond}
def naive_datetime_from_rata_die({days, day_fraction}) do
{year, month, day} = date_from_rata_die_days(days)
def naive_datetime_from_iso_days({days, day_fraction}) do
{year, month, day} = date_from_iso_days_days(days)
{hour, minute, second, microsecond} = time_from_day_fraction(day_fraction)
{year, month, day, hour, minute, second, microsecond}
end
@@ -116,23 +114,22 @@ defmodule Calendar.ISO do
{hours, minutes, seconds, {microseconds, 6}}
end
# Converts a year, month, day in only a count of days since the Rata Die epoch.
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_rata_die_days(0, 1, 1) do
-365
def date_to_iso_days_days(0, 1, 1) do
0
end
def date_to_rata_die_days(1970, 1, 1) do
719163
def date_to_iso_days_days(1970, 1, 1) do
719528
end
def date_to_rata_die_days(year, month, day) do
# Rata Die starts at year 1, rather than at year 0.
:calendar.date_to_gregorian_days(year, month, day) - 365
def date_to_iso_days_days(year, month, day) do
:calendar.date_to_gregorian_days(year, month, day)
end
# Calculates {year, month, day} from the count of days since the Rata Die epoch.
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_rata_die_days(days) do
:calendar.gregorian_days_to_date(days + 365)
def date_from_iso_days_days(days) do
:calendar.gregorian_days_to_date(days)
end
defp div_mod(int1, int2) do
@@ -273,8 +270,9 @@ defmodule Calendar.ISO do
year <= 9999 and :calendar.valid_date(year, month, day)
end
def valid_time?(hour, minute, second, {microsecond, _}) do
hour in 0..23 and minute in 0..59 and second in 0..60 and microsecond in 0..999_999
def valid_time?(hour, minute, second, {microsecond, precision}) do
hour in 0..23 and minute in 0..59 and second in 0..60 and
microsecond in 0..999_999 and precision in 0..6
end
def day_rollover_relative_to_midnight_utc() do
@@ -418,26 +416,26 @@ defmodule Calendar.ISO do
end
@doc false
def rata_die_to_unit({days, {parts, ppd}}, unit) do
def iso_days_to_unit({days, {parts, ppd}}, unit) do
day_microseconds = days * @seconds_per_day * @microseconds_per_second
microseconds = div(parts * @seconds_per_day * @microseconds_per_second, ppd)
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
end
@doc false
def add_day_fraction_to_rata_die({days, {parts, ppd}}, add, ppd) do
normalize_rata_die(days, parts + add, ppd)
def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, ppd) do
normalize_iso_days(days, parts + add, ppd)
end
def add_day_fraction_to_rata_die({days, {parts, ppd}}, add, add_ppd) do
def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, add_ppd) do
parts = parts * add_ppd
add = add * ppd
gcd = Integer.gcd(ppd, add_ppd)
result_parts = div(parts + add, gcd)
result_ppd = div(ppd * add_ppd, gcd)
normalize_rata_die(days, result_parts, result_ppd)
normalize_iso_days(days, result_parts, result_ppd)
end
defp normalize_rata_die(days, parts, ppd) do
defp normalize_iso_days(days, parts, ppd) do
days_offset = div(parts, ppd)
parts = rem(parts, ppd)
if parts < 0 do
+53 -24
View File
@@ -27,6 +27,11 @@ defmodule NaiveDateTime do
`NaiveDateTime` is not validated against a time zone, such errors
would go unnoticed.
The functions on this module work with the `NaiveDateTime` struct as well
as any struct that contains the same fields as the `NaiveDateTime` struct,
such as `DateTime`. Such functions expect
`t:Calendar.naive_datetime/0` in their typespecs (instead of `t:t/0`).
Developers should avoid creating the NaiveDateTime structs directly
and instead rely on the functions provided by this module as well
as the ones in 3rd party calendar libraries.
@@ -149,7 +154,7 @@ defmodule NaiveDateTime do
def new(date, time)
def new(%Date{calendar: calendar, year: year, month: month, day: day},
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}) do
%Time{calendar: calendar, hour: hour, minute: minute, second: second, microsecond: microsecond}) do
{:ok, %NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}}
end
@@ -195,9 +200,9 @@ defmodule NaiveDateTime do
integer, unit \\ :second) when is_integer(integer) do
ppd = System.convert_time_unit(86400, :second, unit)
naive_datetime
|> to_rata_die()
|> Calendar.ISO.add_day_fraction_to_rata_die(integer, ppd)
|> from_rata_die(calendar, precision)
|> to_iso_days()
|> Calendar.ISO.add_day_fraction_to_iso_days(integer, ppd)
|> from_iso_days(calendar, precision)
end
@doc """
@@ -232,8 +237,8 @@ defmodule NaiveDateTime do
raise ArgumentError, "cannot calculate the difference between #{inspect naive_datetime1} and #{inspect naive_datetime2} because their calendars are not compatible and thus the result would be ambiguous"
end
units1 = naive_datetime1 |> to_rata_die() |> Calendar.ISO.rata_die_to_unit(unit)
units2 = naive_datetime2 |> to_rata_die() |> Calendar.ISO.rata_die_to_unit(unit)
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
units1 - units2
end
@@ -509,6 +514,7 @@ defmodule NaiveDateTime do
{:error, :invalid_date}
iex> NaiveDateTime.from_erl({{2000, 13, 1},{13, 30, 15}})
{:error, :invalid_date}
"""
@spec from_erl(:calendar.datetime, Calendar.microsecond) :: {:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -532,6 +538,7 @@ defmodule NaiveDateTime do
~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
"""
@spec from_erl!(:calendar.datetime, Calendar.microsecond) :: t | no_return
def from_erl!(tuple, microsecond \\ {0, 0}) do
@@ -574,7 +581,7 @@ defmodule NaiveDateTime do
@spec compare(Calendar.naive_datetime, Calendar.naive_datetime) :: :lt | :eq | :gt
def compare(%{calendar: calendar1} = naive_datetime1, %{calendar: calendar2} = naive_datetime2) do
if Calendar.compatible_calendars?(calendar1, calendar2) do
case {to_rata_die(naive_datetime1), to_rata_die(naive_datetime2)} do
case {to_iso_days(naive_datetime1), to_iso_days(naive_datetime2)} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
@@ -590,23 +597,36 @@ defmodule NaiveDateTime do
end
@doc """
Converts a `NaiveDateTime` struct from one calendar to another.
Converts the given `naive_datetime` from one calendar to another.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an `{:error, :incompatible_calendars}` tuple
is returned.
## 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> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
{:ok, %NaiveDateTime{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1,
hour: 13, minute: 30, second: 15, microsecond: {0, 0}}}
"""
@spec convert(Calendar.naive_datetime, Calendar.calendar) :: {:ok, NaiveDateTime.t} | {:error, :incompatible_calendars}
def convert(%NaiveDateTime{calendar: calendar} = naive_datetime, calendar) do
{:ok, naive_datetime}
@spec convert(Calendar.naive_datetime, Calendar.calendar) :: {:ok, t} | {:error, :incompatible_calendars}
def convert(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}, calendar) do
{:ok, %NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}}
end
def convert(%{calendar: ndt_calendar, microsecond: {_, precision}} = naive_datetime, calendar) do
if Calendar.compatible_calendars?(ndt_calendar, calendar) do
result_naive_datetime =
naive_datetime
|> to_rata_die
|> from_rata_die(calendar, precision)
|> to_iso_days
|> from_iso_days(calendar, precision)
{:ok, result_naive_datetime}
else
{:error, :incompatible_calendars}
@@ -614,40 +634,49 @@ defmodule NaiveDateTime do
end
@doc """
Converts a NaiveDateTime from one calendar to another.
Converts the given `naive_datetime` from one calendar to another.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an ArgumentError is raised.
## 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> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
%NaiveDateTime{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1,
hour: 13, minute: 30, second: 15, microsecond: {0, 0}}
"""
@spec convert!(NaiveDateTime.t, Calendar.calendar) :: NaiveDateTime.t
@spec convert!(Calendar.naive_datetime, Calendar.calendar) :: t
def convert!(naive_datetime, calendar) do
case convert(naive_datetime, calendar) do
{:ok, value} ->
value
{:error, :incompatible_calendars} ->
raise ArgumentError, "cannot convert #{inspect naive_datetime} to target calendar #{inspect calendar}, reason: #{inspect naive_datetime.calendar} and #{inspect calendar} have different day rollover moments, making this conversion ambiguous"
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect naive_datetime} to target calendar #{inspect calendar}, reason: #{inspect reason}"
end
end
## Helpers
defp to_rata_die(%{calendar: calendar, year: year, month: month, day: day,
defp to_iso_days(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_rata_die(year, month, day, hour, minute, second, microsecond)
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
end
defp from_rata_die(rata_die, calendar, precision) do
defp from_iso_days(iso_days, calendar, precision) do
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_rata_die(rata_die)
%NaiveDateTime{year: year, month: month, day: day, hour: hour, minute: minute, second: second,
microsecond: {microsecond, precision}, calendar: calendar}
calendar.naive_datetime_from_iso_days(iso_days)
%NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision}}
end
defimpl String.Chars do
def to_string(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
end
end
+62 -15
View File
@@ -63,7 +63,9 @@ defmodule Time do
Expects all values to be integers. Returns `{:ok, time}` if each
entry fits its appropriate range, returns `{:error, reason}` otherwise.
Note a time may have 60 seconds in case of leap seconds.
Note a time may have 60 seconds in case of leap seconds. Microseconds
can also be given with a precision, which must be an integer between
0 and 6.
## Examples
@@ -87,9 +89,13 @@ defmodule Time do
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, Calendar.calendar) ::
{:ok, Time.t} | {:error, atom}
{: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
@@ -108,7 +114,7 @@ defmodule Time do
end
@doc """
Converts the given time to a string.
Converts the given `time` to a string.
### Examples
@@ -252,7 +258,7 @@ defmodule Time do
end
@doc """
Converts a `Time` struct to an Erlang time tuple.
Converts given `time` to an Erlang time tuple.
WARNING: Loss of precision may occur, as Erlang time tuples
only contain hours/minutes/seconds.
@@ -315,7 +321,7 @@ defmodule Time do
end
@doc """
Compares two `Time` structs.
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
@@ -325,12 +331,16 @@ defmodule Time do
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])
@@ -359,14 +369,24 @@ defmodule Time do
end
@doc """
Converts the `Time` struct to a different calendar.
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}}}
"""
@spec convert(Calendar.time, Calendar.calendar) :: {:ok, Time.t} | {:error, atom}
def convert(%Time{calendar: calendar} = time, calendar) do
{:ok, time}
@spec convert(Calendar.time, Calendar.calendar) :: {:ok, t} | {:error, atom}
def convert(%{calendar: calendar, hour: hour, minute: minute, second: second, microsecond: microsecond}, calendar) do
{:ok, %Time{calendar: calendar, hour: hour, minute: minute, second: second, microsecond: microsecond}}
end
def convert(%{microsecond: {_, precision}} = time, calendar) do
@@ -381,8 +401,18 @@ defmodule Time do
@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}}
"""
@spec convert!(Calendar.time, Calendar.calendar) :: Time.t
@spec convert!(Calendar.time, Calendar.calendar) :: t
def convert!(time, calendar) do
case convert(time, calendar) do
{:ok, value} ->
@@ -393,7 +423,14 @@ defmodule Time do
end
@doc """
Returns the difference between two `Time` structs.
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, month, second, and microsecond is considered. Any
additional information about a date or time zone is ignored when calculating
the difference.
The answer can be returned in any `unit` available from
`t:System.time_unit/0`. If the first unit is smaller than
@@ -406,18 +443,28 @@ defmodule Time do
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
"""
@spec diff(Time.t, Time.t, System.time_unit) :: integer
def diff(%Time{} = time1, %Time{} = time2, unit \\ :second) do
@spec diff(Calendar.time, Calendar.time, System.time_unit) :: integer
def diff(time1, time2, unit \\ :second) do
fraction1 = to_day_fraction(time1)
fraction2 = to_day_fraction(time2)
Calendar.ISO.rata_die_to_unit({0, fraction1}, unit) -
Calendar.ISO.rata_die_to_unit({0, fraction2}, unit)
Calendar.ISO.iso_days_to_unit({0, fraction1}, unit) -
Calendar.ISO.iso_days_to_unit({0, fraction2}, unit)
end
## Helpers
+59 -53
View File
@@ -314,12 +314,22 @@ defmodule Enum do
end
end
@doc """
Shortcut to `chunk(enumerable, count, count)`.
"""
@spec chunk(t, pos_integer) :: [list]
# Deprecate on v1.7
@doc false
def chunk(enumerable, count), do: chunk(enumerable, count, count, nil)
# Deprecate on v1.7
@doc false
def chunk(enumerable, count, step, leftover \\ nil) do
chunk_every(enumerable, count, step, leftover || :discard)
end
@doc """
Shortcut to `chunk_every(enumerable, count, count)`.
"""
@spec chunk_every(t, pos_integer) :: [list]
def chunk_every(enumerable, count), do: chunk_every(enumerable, count, count, [])
@doc """
Returns list of lists containing `count` items each, where
each new chunk starts `step` elements into the enumerable.
@@ -327,55 +337,48 @@ defmodule Enum do
`step` is optional and, if not passed, defaults to `count`, i.e.
chunks do not overlap.
If the final chunk does not have `count` elements to fill the chunk,
the final chunk is dropped unless `leftover` is given.
If the last chunk does not have `count` elements to fill the chunk,
elements are taken from `leftover` to fill in the chunk. If `leftover`
does not have enough elements to fill the chunk, then a partial chunk
is returned with less than `count` elements.
If `leftover` is given, elements are taken from `leftover` to fill in
the chunk. If `leftover` is passed and does not have enough elements
to fill the chunk, then a partial chunk is returned with less than
`count` elements. Therefore, an empty list can be given to `leftover`
when one simply desires for the last chunk to not be discarded.
If `:discard` is given in `leftover`, the last chunk is discarded
unless it has exactly `count` elements.
## Examples
iex> Enum.chunk([1, 2, 3, 4, 5, 6], 2)
iex> Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)
[[1, 2], [3, 4], [5, 6]]
iex> Enum.chunk([1, 2, 3, 4, 5, 6], 3, 2)
iex> Enum.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, :discard)
[[1, 2, 3], [3, 4, 5]]
iex> Enum.chunk([1, 2, 3, 4, 5, 6], 3, 2, [7])
iex> Enum.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, [7])
[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
iex> Enum.chunk([1, 2, 3, 4], 3, 3, [])
iex> Enum.chunk_every([1, 2, 3, 4], 3, 3, [])
[[1, 2, 3], [4]]
iex> Enum.chunk([1, 2, 3, 4], 10)
[]
iex> Enum.chunk([1, 2, 3, 4], 10, 10, [])
iex> Enum.chunk_every([1, 2, 3, 4], 10)
[[1, 2, 3, 4]]
"""
@spec chunk(t, pos_integer, pos_integer, t | nil) :: [list]
def chunk(enumerable, count, step, leftover \\ nil)
@spec chunk_every(t, pos_integer, pos_integer, t | :discard) :: [list]
def chunk_every(enumerable, count, step, leftover \\ [])
when is_integer(count) and count > 0 and is_integer(step) and step > 0 do
limit = :erlang.max(count, step)
{acc, {buffer, i}} =
reduce(enumerable, {[], {[], 0}}, R.chunk(count, step, limit))
if is_nil(leftover) || i == 0 do
:lists.reverse(acc)
else
buffer = :lists.reverse(buffer, take(leftover, count - i))
:lists.reverse([buffer | acc])
end
R.chunk_every(&chunk_while/4, enumerable, count, step, leftover)
end
@doc """
Splits enumerable on every element for which `fun` returns a new
value.
Chunks the `enum` with fine grained control when every chunk is emitted.
`chunk_fun` receives the current element and the accumulator and
must return `{:cont, element, acc}` to emit the given chunk and
continue with accumulator or `{:cont, acc}` to not emit any chunk
and continue with the return accumulator.
`after_fun` is invoked when iteration is done and must also return
`{:cont, element, acc}` or `{:cont, acc}`.
Returns a list of lists.
@@ -392,15 +395,22 @@ defmodule Enum do
...> [] -> {:cont, []}
...> acc -> {:cont, Enum.reverse(acc), []}
...> end
iex> Enum.chunk_by(1..10, [], chunk_fun, after_fun)
iex> Enum.chunk_while(1..10, [], chunk_fun, after_fun)
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
"""
@spec chunk_by(t, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc})) :: Enumerable.t when chunk: any
def chunk_by(enum, acc, chunk_fun, after_fun) do
{res, acc} = reduce(enum, {[], acc}, R.chunk_by(chunk_fun))
@spec chunk_while(t, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc})) :: Enumerable.t when chunk: any
def chunk_while(enum, acc, chunk_fun, after_fun) do
{_, {res, acc}} =
Enumerable.reduce(enum, {:cont, {[], acc}}, fn entry, {buffer, acc} ->
case chunk_fun.(entry, acc) do
{:cont, emit, acc} -> {:cont, {[emit | buffer], acc}}
{:cont, acc} -> {:cont, {buffer, acc}}
{:halt, acc} -> {:halt, {buffer, acc}}
end
end)
case after_fun.(acc) do
{:cont, _acc} -> :lists.reverse(res)
@@ -422,18 +432,7 @@ defmodule Enum do
"""
@spec chunk_by(t, (element -> any)) :: [list]
def chunk_by(enumerable, fun) do
chunk_by(enumerable, nil, fn
entry, nil ->
{:cont, {[entry], fun.(entry)}}
entry, {acc, value} ->
case fun.(entry) do
^value -> {:cont, {[entry | acc], value}}
new_value -> {:cont, :lists.reverse(acc), {[entry], new_value}}
end
end, fn
nil -> {:cont, :done}
{acc, _value} -> {:cont, :lists.reverse(acc), :done}
end)
R.chunk_by(&chunk_while/4, enumerable, fun)
end
@doc """
@@ -3192,6 +3191,13 @@ defimpl Enumerable, for: Function do
def member?(_function, _value),
do: {:error, __MODULE__}
def reduce(function, acc, fun),
def reduce(function, acc, fun) when is_function(function, 2),
do: function.(acc, fun)
def reduce(function, _acc, _fun) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: function,
description: "only anonymous functions of arity 2 are enumerable"
end
end
+2 -3
View File
@@ -230,7 +230,7 @@ defmodule Exception do
{_, kind, _, clauses} <- List.keyfind(defs, {function, arity}, 0) do
clauses =
for {meta, ex_args, guards, _block} <- clauses do
scope = :elixir_erl.definition_scope(meta, kind, function, arity, "nofile")
scope = :elixir_erl.definition_scope(meta, "nofile")
{erl_args, scope} =
:elixir_erl_clauses.match(&:elixir_erl_pass.translate_args/2, ex_args, scope)
{args, binding} =
@@ -862,8 +862,7 @@ defmodule UndefinedFunctionError do
end
defp format_fa({_dist, fun, arity}) do
fun = with ":" <> fun <- inspect(fun), do: fun
[" * ", fun, ?/, Integer.to_string(arity), ?\n]
[" * ", Inspect.Function.escape_name(fun), ?/, Integer.to_string(arity), ?\n]
end
defp exports_for(module) do
+23 -5
View File
@@ -670,15 +670,21 @@ defmodule File do
File.cp_r "samples", "tmp"
# Same as before, but asks the user how to proceed in case of conflicts
File.cp_r "samples", "tmp", fn(source, destination) ->
File.cp_r "samples", "tmp", fn source, destination ->
IO.gets("Overwriting #{destination} by #{source}. Type y to confirm. ") == "y\n"
end
"""
@spec cp_r(Path.t, Path.t, (Path.t, Path.t -> boolean)) :: {:ok, [binary]} | {:error, posix, binary}
def cp_r(source, destination, callback \\ fn(_, _) -> true end) when is_function(callback, 2) do
source = IO.chardata_to_string(source)
destination = IO.chardata_to_string(destination)
def cp_r(source, destination, callback \\ fn _, _ -> true end) when is_function(callback, 2) do
source =
source
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
destination =
destination
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
case do_cp_r(source, destination, callback, []) do
{:error, _, _} = error -> error
@@ -947,7 +953,10 @@ defmodule File do
"""
@spec rm_rf(Path.t) :: {:ok, [binary]} | {:error, posix, binary}
def rm_rf(path) do
do_rm_rf(IO.chardata_to_string(path), {:ok, []})
path
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
|> do_rm_rf({:ok, []})
end
defp do_rm_rf(path, {:ok, _} = entry) do
@@ -1454,6 +1463,15 @@ defmodule File do
@read_ahead_size 64 * 1024
defp assert_no_null_byte!(binary, operation) do
case :binary.match(binary, "\0") do
{_, _} ->
raise ArgumentError, "cannot execute #{operation} for path with null byte, got: #{inspect binary}"
:nomatch ->
binary
end
end
defp normalize_modes([:utf8 | rest], binary?) do
[encoding: :utf8] ++ normalize_modes(rest, binary?)
end
+6 -5
View File
@@ -356,20 +356,21 @@ defimpl Inspect, for: Map do
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
surround_many(open, map, close, opts, traverse_fun(map), sep)
surround_many(open, map, close, opts, traverse_fun(map, opts), sep)
end
defp traverse_fun(list) do
defp traverse_fun(list, opts) do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
&to_map/2
sep = color(" => ", :map, opts)
&to_map(&1, &2, sep)
end
end
defp to_map({key, value}, opts) do
defp to_map({key, value}, opts, sep) do
concat(
concat(to_doc(key, opts), " => "),
concat(to_doc(key, opts), sep),
to_doc(value, opts)
)
end
+1 -1
View File
@@ -243,7 +243,7 @@ defmodule Inspect.Algebra do
try do
Process.put(:inspect_trap, true)
res = Inspect.Map.inspect(map, opts)
res = Inspect.Map.inspect(map, %{opts | syntax_colors: []})
res = IO.iodata_to_binary(format(res, :infinity))
exception = Inspect.Error.exception(
+28 -8
View File
@@ -3289,8 +3289,17 @@ defmodule Kernel do
result
end
{escaped, _} = :elixir_quote.escape(block, false)
module_vars = module_vars(env.vars, 0)
escaped =
case env do
%{function: nil, lexical_tracker: pid} when is_pid(pid) ->
integer = Kernel.LexicalTracker.write_cache(pid, block)
quote do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer))
%{} ->
{escaped, _} = :elixir_quote.escape(block, false)
escaped
end
module_vars = module_vars(env.vars, 0)
quote do
unquote(with_alias)
@@ -3554,19 +3563,30 @@ defmodule Kernel do
end
defp define(kind, call, expr, env) do
assert_module_scope(env, kind, 2)
module = assert_module_scope(env, kind, 2)
assert_no_function_scope(env, kind, 2)
table = :elixir_module.data_table(module)
{call, unquoted_call} = :elixir_quote.escape(call, true)
{expr, unquoted_expr} = :elixir_quote.escape(expr, true)
{escaped_call, unquoted_call} = :elixir_quote.escape(call, true)
{escaped_expr, unquoted_expr} = :elixir_quote.escape(expr, true)
escaped_expr =
case unquoted_expr do
true ->
escaped_expr
false ->
key = {:cache_def, :erlang.unique_integer()}
:ets.insert(table, {key, expr})
quote do: :ets.lookup_element(unquote(table), unquote(key), 2)
end
# Do not check clauses if any expression was unquoted
check_clauses = not(unquoted_expr or unquoted_call)
pos = :elixir_locals.cache_env(env)
pos = :elixir_locals.cache_env(table, env)
quote do
:elixir_def.store_definition(unquote(kind), unquote(check_clauses),
unquote(call), unquote(expr), unquote(pos))
unquote(escaped_call), unquote(escaped_expr), unquote(pos))
end
end
@@ -4594,7 +4614,7 @@ defmodule Kernel do
defp assert_module_scope(env, fun, arity) do
case env.module do
nil -> raise ArgumentError, "cannot invoke #{fun}/#{arity} outside module"
_ -> :ok
mod -> mod
end
end
+21 -1
View File
@@ -81,6 +81,18 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:alias_dispatch, module})
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, :import)
@@ -99,7 +111,7 @@ defmodule Kernel.LexicalTracker do
def init(dest) do
{:ok, %{directives: %{}, references: %{}, compile: %{},
runtime: %{}, dest: dest}}
runtime: %{}, dest: dest, cache: %{}}}
end
@doc false
@@ -124,6 +136,14 @@ defmodule Kernel.LexicalTracker do
{:reply, state.dest, state}
end
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
{:reply, Map.fetch!(cache, key), state}
end
def handle_cast({:write_cache, key, value}, %{cache: cache} = state) do
{:noreply, Map.put(state, :cache, Map.put(cache, key, value))}
end
def handle_cast({:remote_reference, module, mode}, state) do
{:noreply, %{state | references: add_reference(state.references, module, mode)}}
end
+6
View File
@@ -44,6 +44,9 @@ defmodule Macro.Env do
* `export_vars` - a list keeping all variables to be exported in a
construct (may be `nil`)
* `match_vars` - controls how "new" variables are handled. Inside a
match it is a list with all variables in a match. Outside of a match
is either `:warn` or `:apply`
* `prematch_vars` - a list of variables defined before a match (is
`nil` when not inside a match)
@@ -64,6 +67,7 @@ defmodule Macro.Env do
@type local :: atom | nil
@opaque export_vars :: vars | nil
@opaque match_vars :: vars | :warn | :apply
@opaque prematch_vars :: vars | nil
@type t :: %{__struct__: __MODULE__,
@@ -80,6 +84,7 @@ defmodule Macro.Env do
context_modules: context_modules,
vars: vars,
export_vars: export_vars,
match_vars: match_vars,
prematch_vars: prematch_vars,
lexical_tracker: lexical_tracker}
@@ -99,6 +104,7 @@ defmodule Macro.Env do
vars: [],
lexical_tracker: nil,
export_vars: nil,
match_vars: :warn,
prematch_vars: nil}
end
+2 -1
View File
@@ -548,7 +548,8 @@ defmodule Map do
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
not present in `map`, `initial` is inserted as the value of `key`.
not present in `map`, `initial` is inserted as the value of `key`. The initial
value will not be passed through the update function.
## Examples
+2 -2
View File
@@ -1299,9 +1299,9 @@ defmodule Module do
end
defp known_callbacks(callbacks) do
formatted = for {{{name, arity}, kind}, module} <- callbacks do
"\n * " <> Exception.format_mfa(module, name, arity) <> "(#{kind})"
"\n * " <> Exception.format_mfa(module, name, arity) <> " (#{kind})"
end
". The known callbacks are:\n#{formatted}"
". The known callbacks are:\n#{formatted}\n"
end
@doc false
+21 -47
View File
@@ -166,8 +166,8 @@ defmodule Module.LocalsTracker do
end
defp unreachable(reachable, private) do
for {tuple, kind, _, _} <- private,
kind == :defmacrop or not :sets.is_element(tuple, reachable),
for {tuple, _, _, _} <- private,
not :sets.is_element(tuple, reachable),
do: tuple
end
@@ -210,16 +210,6 @@ defmodule Module.LocalsTracker do
last
end
@doc false
def cache_env(pid, env) do
:gen_server.call(pid, {:cache_env, env}, @timeout)
end
@doc false
def get_cached_env(pid, ref) do
:gen_server.call(pid, {:get_cached_env, ref}, @timeout)
end
# Stops the gen server
@doc false
def stop(pid) do
@@ -231,64 +221,48 @@ defmodule Module.LocalsTracker do
def init([]) do
d = :digraph.new([:protected])
:digraph.add_vertex(d, :local)
{:ok, {d, []}}
{:ok, d}
end
@doc false
def handle_call({:cache_env, env}, _from, {d, cache}) do
case cache do
[{i, ^env} | _] ->
{:reply, i, {d, cache}}
t ->
i = length(t)
{:reply, i, {d, [{i, env} | t]}}
end
end
def handle_call({:get_cached_env, ref}, _from, {_, cache} = state) do
{^ref, env} = :lists.keyfind(ref, 1, cache)
{:reply, env, state}
end
def handle_call({:yank, local}, _from, {d, _} = state) do
def handle_call({:yank, local}, _from, d) do
out_vertices = :digraph.out_neighbours(d, local)
:digraph.del_edges(d, :digraph.out_edges(d, local))
{:reply, {[], out_vertices}, state}
{:reply, {[], out_vertices}, d}
end
def handle_call(:digraph, _from, {d, _} = state) do
{:reply, d, state}
def handle_call(:digraph, _from, d) do
{:reply, d, d}
end
@doc false
def handle_info(_msg, state) do
{:noreply, state}
def handle_info(_msg, d) do
{:noreply, d}
end
def handle_cast({:add_local, from, to}, {d, _} = state) do
def handle_cast({:add_local, from, to}, d) do
handle_add_local(d, from, to)
{:noreply, state}
{:noreply, d}
end
def handle_cast({:add_import, function, module, {name, arity}}, {d, _} = state) do
def handle_cast({:add_import, function, module, {name, arity}}, d) do
handle_import(d, function, module, name, arity)
{:noreply, state}
{:noreply, d}
end
def handle_cast({:add_definition, kind, tuple}, {d, _} = state) do
def handle_cast({:add_definition, kind, tuple}, d) do
handle_add_definition(d, kind, tuple)
{:noreply, state}
{:noreply, d}
end
def handle_cast({:add_defaults, kind, {name, arity}, defaults}, {d, _} = state) do
def handle_cast({:add_defaults, kind, {name, arity}, defaults}, d) do
for i <- :lists.seq(arity - defaults, arity - 1) do
handle_add_definition(d, kind, {name, i})
handle_add_local(d, {name, i}, {name, arity})
end
{:noreply, state}
{:noreply, d}
end
def handle_cast({:reattach, _kind, tuple, {in_neigh, out_neigh}}, {d, _} = state) do
def handle_cast({:reattach, _kind, tuple, {in_neigh, out_neigh}}, d) do
for from <- in_neigh do
:digraph.add_vertex(d, from)
replace_edge!(d, from, tuple)
@@ -299,11 +273,11 @@ defmodule Module.LocalsTracker do
replace_edge!(d, tuple, to)
end
{:noreply, state}
{:noreply, d}
end
def handle_cast(:stop, state) do
{:stop, :normal, state}
def handle_cast(:stop, d) do
{:stop, :normal, d}
end
@doc false
+9 -5
View File
@@ -615,17 +615,19 @@ defmodule Path do
def wildcard(glob, opts \\ []) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
glob
|> chardata_to_list()
|> chardata_to_list!()
|> :filelib.wildcard(mod)
|> Enum.map(&IO.chardata_to_string/1)
end
# expand_dot the given path by expanding "..", "." and "~".
defp chardata_to_list(chardata) do
defp chardata_to_list!(chardata) do
case :unicode.characters_to_list(chardata) do
result when is_list(result) ->
result
if 0 in result do
raise ArgumentError, "cannot execute Path.wildcard/2 for path with null byte, got: #{inspect chardata}"
else
result
end
{:error, encoded, rest} ->
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
@@ -654,6 +656,8 @@ defmodule Path do
end
end
# expand_dot the given path by expanding "..", "." and "~".
defp expand_dot(<<"/", rest::binary>>),
do: "/" <> do_expand_dot(rest)
defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
+13 -11
View File
@@ -254,21 +254,22 @@ defmodule Protocol do
{:error, :not_a_protocol} |
{:error, :no_beam_info}
def consolidate(protocol, types) when is_atom(protocol) do
with {:ok, info} <- beam_protocol(protocol),
{:ok, code, docs} <- change_debug_info(info, types),
do: compile(code, docs)
with {:ok, ast_info, chunks_info} <- beam_protocol(protocol),
{:ok, code} <- change_debug_info(ast_info, types),
do: compile(protocol, code, chunks_info)
end
defp beam_protocol(protocol) do
chunk_ids = [:abstract_code, :attributes, 'ExDc']
chunk_ids = [:abstract_code, :attributes, :compile_info, 'ExDc']
opts = [:allow_missing_chunks]
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
{:ok, {^protocol, [{:abstract_code, {_raw, abstract_code}},
{:attributes, attributes},
{:compile_info, compile_info},
{'ExDc', docs}]}} ->
case attributes[:protocol] do
[fallback_to_any: any] ->
{:ok, {protocol, any, abstract_code, docs}}
{:ok, {protocol, any, abstract_code}, {compile_info, docs}}
_ ->
{:error, :not_a_protocol}
end
@@ -286,12 +287,12 @@ defmodule Protocol do
# Change the debug information to the optimized
# impl_for/1 dispatch version.
defp change_debug_info({protocol, any, code, docs}, types) do
defp change_debug_info({protocol, any, code}, types) do
types = if any, do: types, else: List.delete(types, Any)
all = [Any] ++ for {_guard, mod} <- __builtin__(), do: mod
structs = types -- all
case change_impl_for(code, protocol, types, structs, false, []) do
{:ok, ret} -> {:ok, ret, docs}
{:ok, ret} -> {:ok, ret}
other -> other
end
end
@@ -358,9 +359,9 @@ defmodule Protocol do
change_impl_for(tail, protocol, info, types, protocol?, [head | acc])
end
defp change_impl_for([], protocol, _info, _types, protocol?, acc) do
defp change_impl_for([], _protocol, _info, _types, protocol?, acc) do
if protocol? do
{:ok, {protocol, Enum.reverse(acc)}}
{:ok, Enum.reverse(acc)}
else
{:error, :not_a_protocol}
end
@@ -405,8 +406,9 @@ defmodule Protocol do
end
# Finally compile the module and emit its bytecode.
defp compile({protocol, code}, docs) do
opts = if Code.compiler_options[:debug_info], do: [:debug_info], else: []
defp compile(protocol, code, {compile_info, docs}) do
opts = Keyword.take(compile_info, [:source])
opts = if Code.compiler_options[:debug_info], do: [:debug_info | opts], else: opts
{:ok, ^protocol, binary, _warnings} = :compile.forms(code, [:return | opts])
{:ok,
case docs do
+120 -2
View File
@@ -534,7 +534,8 @@ defmodule Registry do
"""
@spec match(registry, key, match_pattern :: atom() | tuple(), guards :: list()) :: [{pid, term}]
def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do
spec = [{{key, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
@@ -587,7 +588,16 @@ defmodule Registry do
def keys(registry, pid) when is_atom(registry) and is_pid(pid) do
{kind, partitions, _, pid_ets, _} = info!(registry)
{_, pid_ets} = pid_ets || pid_ets!(registry, pid, partitions)
keys = safe_lookup_second(pid_ets, pid)
keys = try do
spec = [{{pid, :'$1', :'$2'}, [], [{{:'$1', :'$2'}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
# Handle the possibility of fake keys
keys = gather_keys(keys, [], false)
cond do
kind == :unique -> Enum.uniq(keys)
@@ -595,6 +605,19 @@ defmodule Registry do
end
end
defp gather_keys([{key, {_, remaining}} | rest], acc, _fake) do
gather_keys(rest, [key | acc], {key, remaining})
end
defp gather_keys([{key, _} | rest], acc, fake) do
gather_keys(rest, [key | acc], fake)
end
defp gather_keys([], acc, {key, remaining}) do
List.duplicate(key, remaining) ++ Enum.reject(acc, & &1 === key)
end
defp gather_keys([], acc, false) do
acc
end
@doc """
Unregisters all entries for the given `key` associated to the current
process in `registry`.
@@ -652,6 +675,95 @@ defmodule Registry do
:ok
end
@doc """
Unregister entries for a given key matching a pattern.
## Examples
For unique registries it can be used to conditionally unregister a key on
the basis of whether or not it matches a particular value.
iex> Registry.start_link(:unique, Registry.UniqueUnregisterMatchTest)
iex> Registry.register(Registry.UniqueUnregisterMatchTest, "hello", :world)
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
["hello"]
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :foo)
:ok
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
["hello"]
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :world)
:ok
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
[]
For duplicate registries:
iex> Registry.start_link(:duplicate, Registry.DuplicateUnregisterMatchTest)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_b)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_c)
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
["hello", "hello", "hello"]
iex> Registry.unregister_match(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
:ok
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
["hello", "hello"]
iex> Registry.lookup(Registry.DuplicateUnregisterMatchTest, "hello")
[{self(), :world_b}, {self(), :world_c}]
"""
def unregister_match(registry, key, pattern, guards \\ []) when is_list(guards) do
self = self()
{kind, partitions, key_ets, pid_ets, listeners} = info!(registry)
{key_partition, pid_partition} = partitions(kind, key, self, partitions)
key_ets = key_ets || key_ets!(registry, key_partition)
{pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition)
# Remove first from the key_ets because in case of crashes
# the pid_ets will still be able to clean up. The last step is
# to clean if we have no more entries.
# Here we want to count all entries for this pid under this key, regardless
# of pattern.
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
total = :ets.select_count(key_ets, total_spec)
# We only want to delete things that match the pattern
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards] ,[true]}]
case :ets.select_delete(key_ets, delete_spec) do
# We deleted everything, we can just delete the object
^total ->
true = :ets.delete_object(pid_ets, {self, key, key_ets})
unlink_if_unregistered(pid_server, pid_ets, self)
for listener <- listeners do
Kernel.send(listener, {:unregister, registry, key, self})
end
0 ->
:ok
deleted ->
# There are still entries remaining for this pid. delete_object/2 with
# duplicate_bag tables will remove every entry, but we only want to
# remove those we have deleted. The solution is to introduce a temp_entry
# that indicates how many keys WILL be remaining after the delete operation.
remaining = total - deleted
temp_entry = {self, key, {key_ets, remaining}}
true = :ets.insert(pid_ets, temp_entry)
true = :ets.delete_object(pid_ets, {self, key, key_ets})
real_keys = List.duplicate({self, key, key_ets}, remaining)
true = :ets.insert(pid_ets, real_keys)
# We've recreated the real remaining key entries, so we can now delete
# our temporary entry.
true = :ets.delete_object(pid_ets, temp_entry)
end
:ok
end
@doc """
Registers the current process under the given `key` in `registry`.
@@ -976,6 +1088,12 @@ defmodule Registry.Partition do
def handle_info({:EXIT, pid, _reason}, ets) do
entries = :ets.take(ets, pid)
for {_pid, key, key_ets} <- entries do
key_ets = case key_ets do
# In case the fake key ets is being used. See unregister_match/2.
{key_ets, _} -> key_ets
_ -> key_ets
end
try do
:ets.match_delete(key_ets, {key, {pid, :_}})
catch
+46 -57
View File
@@ -121,63 +121,57 @@ defmodule Stream do
## Transformers
@doc """
Shortcut to `chunk(enum, n, n)`.
"""
@spec chunk(Enumerable.t, pos_integer) :: Enumerable.t
# Deprecate on v1.7
@doc false
def chunk(enum, n), do: chunk(enum, n, n, nil)
# Deprecate on v1.7
@doc false
def chunk(enum, n, step, leftover \\ nil)
when is_integer(n) and n > 0 and is_integer(step) and step > 0 do
chunk_every(enum, n, step, leftover || :discard)
end
@doc """
Streams the enumerable in chunks, containing `n` items each, where
each new chunk starts `step` elements into the enumerable.
Shortcut to `chunk_every(enum, count, count)`.
"""
@spec chunk_every(Enumerable.t, pos_integer) :: Enumerable.t
def chunk_every(enum, count), do: chunk_every(enum, count, count, [])
@doc """
Streams the enumerable in chunks, containing `count` items each,
where each new chunk starts `step` elements into the enumerable.
`step` is optional and, if not passed, defaults to `count`, i.e.
chunks do not overlap.
If the final chunk does not have `count` elements to fill the chunk,
the final chunk is dropped unless `leftover` is given.
If the last chunk does not have `count` elements to fill the chunk,
elements are taken from `leftover` to fill in the chunk. If `leftover`
does not have enough elements to fill the chunk, then a partial chunk
is returned with less than `count` elements.
If `leftover` is given, elements are taken from `leftover` to fill in
the chunk. If `leftover` is passed and does not have enough elements
to fill the chunk, then a partial chunk is returned with less than
`count` elements. Therefore, an empty list can be given to `leftover`
when one simply desires for the last chunk to not be discarded.
If `:discard` is given in `leftover`, the last chunk is discarded
unless it has exactly `count` elements.
## Examples
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 2) |> Enum.to_list
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 2) |> Enum.to_list
[[1, 2], [3, 4], [5, 6]]
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 3, 2) |> Enum.to_list
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, :discard) |> Enum.to_list
[[1, 2, 3], [3, 4, 5]]
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 3, 2, [7]) |> Enum.to_list
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, [7]) |> Enum.to_list
[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list
[[1, 2, 3], [4, 5, 6]]
"""
@spec chunk(Enumerable.t, pos_integer, pos_integer, Enumerable.t | nil) :: Enumerable.t
def chunk(enum, n, step, leftover \\ nil)
when is_integer(n) and n > 0 and is_integer(step) and step > 0 do
limit = :erlang.max(n, step)
if is_nil(leftover) do
lazy enum, {[], 0}, fn(f1) -> R.chunk(n, step, limit, f1) end
else
lazy enum, {[], 0},
fn(f1) -> R.chunk(n, step, limit, f1) end,
&do_chunk(&1, n, leftover, &2)
end
end
defp do_chunk(acc(_, {_, 0}, _) = acc, _, _, _) do
{:cont, acc}
end
defp do_chunk(acc(h, {buffer, count} = old, t), n, leftover, f1) do
buffer = :lists.reverse(buffer, Enum.take(leftover, n - count))
next_with_acc(f1, buffer, h, old, t)
@spec chunk_every(Enumerable.t, pos_integer, pos_integer, Enumerable.t | :discard) :: Enumerable.t
def chunk_every(enum, count, step, leftover \\ [])
when is_integer(count) and count > 0 and is_integer(step) and step > 0 do
R.chunk_every(&chunk_while/4, enum, count, step, leftover)
end
@doc """
@@ -194,18 +188,7 @@ defmodule Stream do
"""
@spec chunk_by(Enumerable.t, (element -> any)) :: Enumerable.t
def chunk_by(enum, fun) do
chunk_by(enum, nil, fn
entry, nil ->
{:cont, {[entry], fun.(entry)}}
entry, {acc, value} ->
case fun.(entry) do
^value -> {:cont, {[entry | acc], value}}
new_value -> {:cont, :lists.reverse(acc), {[entry], new_value}}
end
end, fn
nil -> {:cont, :done}
{acc, _value} -> {:cont, :lists.reverse(acc), :done}
end)
R.chunk_by(&chunk_while/4, enum, fun)
end
@doc """
@@ -232,21 +215,21 @@ defmodule Stream do
...> [] -> {:cont, []}
...> acc -> {:cont, Enum.reverse(acc), []}
...> end
iex> stream = Stream.chunk_by(1..10, [], chunk_fun, after_fun)
iex> stream = Stream.chunk_while(1..10, [], chunk_fun, after_fun)
iex> Enum.to_list(stream)
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
"""
@spec chunk_by(Enumerable.t, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc})) :: Enumerable.t when chunk: any
def chunk_by(enum, acc, chunk_fun, after_fun) do
@spec chunk_while(Enumerable.t, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc})) :: Enumerable.t when chunk: any
def chunk_while(enum, acc, chunk_fun, after_fun) do
lazy enum, acc,
fn(f1) -> R.chunk_by(chunk_fun, f1) end,
&after_chunk_by(&1, &2, after_fun)
fn(f1) -> R.chunk_while(chunk_fun, f1) end,
&after_chunk_while(&1, &2, after_fun)
end
defp after_chunk_by(acc(h, acc, t), f1, after_fun) do
defp after_chunk_while(acc(h, acc, t), f1, after_fun) do
case after_fun.(acc) do
{:cont, emit, acc} -> next_with_acc(f1, emit, h, acc, t)
{:cont, acc} -> {:cont, acc(h, acc, t)}
@@ -1155,6 +1138,10 @@ defmodule Stream do
@spec cycle(Enumerable.t) :: Enumerable.t
def cycle(enumerable)
def cycle([]) do
raise ArgumentError, "cannot cycle over empty enumerable"
end
def cycle(enumerable) when is_list(enumerable) do
unfold {enumerable, enumerable}, fn
{source, [h | t]} -> {h, {source, t}}
@@ -1185,6 +1172,8 @@ defmodule Stream do
{:stream_cycle, acc} ->
{:halted, acc}
else
{state, []} when state in [:done, :halted] ->
raise ArgumentError, "cannot cycle over empty enumerable"
{state, acc} when state in [:done, :halted] ->
do_cycle(cycle, cycle, {:cont, acc})
{:suspended, acc, continuation} ->
+50 -29
View File
@@ -1,36 +1,57 @@
defmodule Stream.Reducers do
# Collection of reducers shared by Enum and Stream.
# Collection of reducers and utilities shared by Enum and Stream.
@moduledoc false
defmacro chunk(amount, step, limit, fun \\ nil) do
quote do
fn entry, acc(head, {buffer, count}, tail) ->
buffer = [entry | buffer]
count = count + 1
def chunk_every(chunk_by, enumerable, count, step, leftover) do
limit = :erlang.max(count, step)
chunk_by.(enumerable, {[], 0}, fn entry, {acc_buffer, acc_count} ->
acc_buffer = [entry | acc_buffer]
acc_count = acc_count + 1
new_state =
if count >= unquote(limit) do
left = count - unquote(step)
{Enum.take(buffer, left), left}
else
{buffer, count}
end
if count == unquote(amount) do
next_with_acc(unquote(fun), :lists.reverse(buffer), head, new_state, tail)
new_state =
if acc_count >= limit do
remaining = acc_count - step
{Enum.take(acc_buffer, remaining), remaining}
else
skip(acc(head, new_state, tail))
{acc_buffer, acc_count}
end
if acc_count == count do
{:cont, :lists.reverse(acc_buffer), new_state}
else
{:cont, new_state}
end
end
end, fn {acc_buffer, acc_count} ->
if leftover == :discard or acc_count == 0 do
{:cont, []}
else
{:cont, :lists.reverse(acc_buffer, Enum.take(leftover, count - acc_count)), []}
end
end)
end
defmacro chunk_by(callback, fun \\ nil) do
def chunk_by(chunk_by, enumerable, fun) do
chunk_by.(enumerable, nil, fn
entry, nil ->
{:cont, {[entry], fun.(entry)}}
entry, {acc, value} ->
case fun.(entry) do
^value -> {:cont, {[entry | acc], value}}
new_value -> {:cont, :lists.reverse(acc), {[entry], new_value}}
end
end, fn
nil -> {:cont, :done}
{acc, _value} -> {:cont, :lists.reverse(acc), :done}
end)
end
defmacro chunk_while(callback, fun \\ nil) do
quote do
fn entry, acc(head, acc, tail) ->
case unquote(callback).(entry, acc) do
{:cont, emit, acc} -> next_with_acc(unquote(fun), emit, head, acc, tail)
{:cont, acc} -> skip(acc(head, acc, tail))
{:halt, acc} -> {:halt, acc(head, acc, tail)}
end
end
end
@@ -38,7 +59,7 @@ defmodule Stream.Reducers do
defmacro dedup(callback, fun \\ nil) do
quote do
fn(entry, acc(head, prev, tail) = acc) ->
fn entry, acc(head, prev, tail) = acc ->
value = unquote(callback).(entry)
case prev do
{:value, ^value} -> skip(acc)
@@ -84,7 +105,7 @@ defmodule Stream.Reducers do
defmacro filter(callback, fun \\ nil) do
quote do
fn(entry, acc) ->
fn entry, acc ->
if unquote(callback).(entry) do
next(unquote(fun), entry, acc)
else
@@ -96,7 +117,7 @@ defmodule Stream.Reducers do
defmacro filter_map(filter, mapper, fun \\ nil) do
quote do
fn(entry, acc) ->
fn entry, acc ->
if unquote(filter).(entry) do
next(unquote(fun), unquote(mapper).(entry), acc)
else
@@ -108,7 +129,7 @@ defmodule Stream.Reducers do
defmacro map(callback, fun \\ nil) do
quote do
fn(entry, acc) ->
fn entry, acc ->
next(unquote(fun), unquote(callback).(entry), acc)
end
end
@@ -127,7 +148,7 @@ defmodule Stream.Reducers do
defmacro reject(callback, fun \\ nil) do
quote do
fn(entry, acc) ->
fn entry, acc ->
unless unquote(callback).(entry) do
next(unquote(fun), entry, acc)
else
@@ -151,7 +172,7 @@ defmodule Stream.Reducers do
defmacro scan3(callback, fun \\ nil) do
quote do
fn(entry, acc(head, acc, tail)) ->
fn entry, acc(head, acc, tail) ->
value = unquote(callback).(entry, acc)
next_with_acc(unquote(fun), value, head, value, tail)
end
@@ -160,7 +181,7 @@ defmodule Stream.Reducers do
defmacro take(fun \\ nil) do
quote do
fn(entry, acc(head, curr, tail) = original) ->
fn entry, acc(head, curr, tail) = original ->
case curr do
0 ->
{:halt, original}
@@ -187,7 +208,7 @@ defmodule Stream.Reducers do
defmacro take_while(callback, fun \\ nil) do
quote do
fn(entry, acc) ->
fn entry, acc ->
if unquote(callback).(entry) do
next(unquote(fun), entry, acc)
else
@@ -199,7 +220,7 @@ defmodule Stream.Reducers do
defmacro uniq_by(callback, fun \\ nil) do
quote do
fn(entry, acc(head, prev, tail) = original) ->
fn entry, acc(head, prev, tail) = original ->
value = unquote(callback).(entry)
if Map.has_key?(prev, value) do
skip(original)
@@ -212,7 +233,7 @@ defmodule Stream.Reducers do
defmacro with_index(fun \\ nil) do
quote do
fn(entry, acc(head, counter, tail)) ->
fn entry, acc(head, counter, tail) ->
next_with_acc(unquote(fun), {entry, counter}, head, counter + 1, tail)
end
end
+43 -54
View File
@@ -274,57 +274,64 @@ defmodule StringIO do
## get_until
defp get_until(encoding, prompt, mod, fun, args,
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
case :unicode.characters_to_list(input, encoding) do
{:error, _, _} ->
{:error, s}
{:incomplete, _, _} ->
{:error, s}
chars ->
{result, input, count} = do_get_until(chars, encoding, mod, fun, args)
defp get_until(encoding, prompt, mod, fun, args, %{input: input} = s) do
case do_get_until(input, encoding, mod, fun, args) do
{result, input, count} ->
input =
case input do
:eof -> ""
_ -> :unicode.characters_to_binary(input, encoding)
end
case input do
:eof -> ""
_ -> list_to_binary(input, encoding)
end
s =
if capture_prompt do
%{s | output: <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>}
else
s
end
{get_until_result(result, encoding), state_after_read(s, input, prompt, count)}
{result, %{s | input: input}}
:error ->
{:error, s}
end
end
defp do_get_until(chars, encoding, mod, fun, args, continuation \\ [], count \\ 0)
defp do_get_until('', encoding, mod, fun, args, continuation, count) do
defp do_get_until("", encoding, mod, fun, args, continuation, count) do
case apply(mod, fun, [continuation, :eof | args]) do
{:done, result, rest} ->
{result, rest, count + 1}
{:more, next_continuation} ->
do_get_until('', encoding, mod, fun, args, next_continuation, count + 1)
do_get_until("", encoding, mod, fun, args, next_continuation, count + 1)
end
end
defp do_get_until(chars, encoding, mod, fun, args, continuation, count) do
{line, rest} = collect_line(chars)
case bytes_until_eol(chars, encoding, 0) do
{r, c} when r in [:split, :replace_split] ->
{line, rest} = :erlang.split_binary(chars, c)
case apply(mod, fun, [continuation, line | args]) do
{:done, result, :eof} ->
{result, rest, count + 1}
{:done, result, extra} ->
{result, extra ++ rest, count + 1}
{:more, next_continuation} ->
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do
{:done, result, :eof} ->
{result, rest, count + 1}
{:done, result, extra} ->
{result, extra ++ binary_to_list(rest, encoding), count + 1}
{:more, next_continuation} ->
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
end
:error ->
:error
end
end
defp binary_to_list(l, _) when is_list(l), do: l
defp binary_to_list(b, :unicode) when is_binary(b), do: to_charlist(b)
defp binary_to_list(b, :latin1) when is_binary(b), do: :binary.bin_to_list(b)
defp list_to_binary(b, _) when is_binary(b), do: b
defp list_to_binary(l, :unicode) when is_list(l), do: to_string(l)
defp list_to_binary(l, :latin1) when is_list(l), do: :binary.list_to_bin(l)
# From http://erlang.org/doc/apps/stdlib/io_protocol.html: Result can be any
# Erlang term, but if it is a list(), the I/O server can convert it to a binary().
defp get_until_result(l, encoding) when is_list(l), do: list_to_binary(l, encoding)
defp get_until_result(other, _), do: other
## io_requests
defp io_requests([r | rs], {:ok, s}) do
@@ -337,12 +344,14 @@ defmodule StringIO do
## helpers
defp state_after_read(%{capture_prompt: false} = s, remainder, _prompt) do
defp state_after_read(state, remainder, prompt, count \\ 1)
defp state_after_read(%{capture_prompt: false} = s, remainder, _prompt, _count) do
%{s | input: remainder}
end
defp state_after_read(%{capture_prompt: true, output: output} = s, remainder, prompt) do
%{s | input: remainder, output: <<output::binary, IO.chardata_to_string(prompt)::binary>>}
defp state_after_read(%{capture_prompt: true, output: output} = s, remainder, prompt, count) do
%{s | input: remainder, output: <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>}
end
defp bytes_until_eol("", _, count), do: {:split, count}
@@ -359,26 +368,6 @@ defmodule StringIO do
defp bytes_until_eol(<<_::binary>>, _, _), do: :error
defp collect_line(chars) do
collect_line(chars, [])
end
defp collect_line([], stack) do
{:lists.reverse(stack), []}
end
defp collect_line([?\r, ?\n | rest], stack) do
{:lists.reverse([?\n | stack]), rest}
end
defp collect_line([?\n | rest], stack) do
{:lists.reverse([?\n | stack]), rest}
end
defp collect_line([h | t], stack) do
collect_line(t, [h | stack])
end
defp io_reply(from, reply_as, reply) do
send from, {:io_reply, reply_as, reply}
end
+19 -2
View File
@@ -339,6 +339,8 @@ defmodule System do
"""
@spec find_executable(binary) :: binary | nil
def find_executable(program) when is_binary(program) do
assert_no_null_byte!(program, "System.find_executable/1")
case :os.find_executable(String.to_charlist(program)) do
false -> nil
other -> List.to_string(other)
@@ -393,8 +395,13 @@ defmodule System do
"""
@spec put_env(binary, binary) :: :ok
def put_env(varname, value) when is_binary(varname) and is_binary(value) do
:os.putenv String.to_charlist(varname), String.to_charlist(value)
:ok
case :binary.match(varname, "=") do
{_, _} ->
raise ArgumentError, "cannot execute System.put_env/2 for key with \"=\", got: #{inspect varname}"
:nomatch ->
:os.putenv String.to_charlist(varname), String.to_charlist(value)
:ok
end
end
@doc """
@@ -588,6 +595,7 @@ defmodule System do
@spec cmd(binary, [binary], keyword) ::
{Collectable.t, exit_status :: non_neg_integer}
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
assert_no_null_byte!(command, "System.cmd/3")
cmd = String.to_charlist(command)
cmd =
@@ -833,6 +841,15 @@ defmodule System do
:erlang.unique_integer(modifiers)
end
defp assert_no_null_byte!(binary, operation) do
case :binary.match(binary, "\0") do
{_, _} ->
raise ArgumentError, "cannot execute #{operation} for program with null byte, got: #{inspect binary}"
:nomatch ->
binary
end
end
defp normalize_time_unit(:native),
do: :native
+2 -1
View File
@@ -33,6 +33,7 @@ Deprecated feature | Deprecated in | Replaced by (
`:char_lists` key in `t:Inspect.Opts.t/0` type | [v1.5] | `:charlists` (v1.3)
`char_list/0` type | [v1.5] | `charlist/0` type (v1.3)
`@compile {:parse_transform, _}` in `Module` | [v1.5] | *None*
EEx: `<%=` in middle and end expressions | [v1.5] | Use `<%` (= is allowed only on start expressions) (v1.0)
`Access.key/1` | [v1.4] | `Access.key/2` (v1.3)
`Behaviour` module | [v1.4] | `@callback` (v1.0)
`Enum.uniq/2` | [v1.4] | `Enum.uniq_by/2` (v1.2)
@@ -68,4 +69,4 @@ Empty string in `String.starts_with?/2`, `String.ends_with?/2`, `String.contains
[v1.2]: https://github.com/elixir-lang/elixir/blob/v1.2/CHANGELOG.md#changelog-for-elixir-v12
[v1.3]: https://github.com/elixir-lang/elixir/blob/v1.3/CHANGELOG.md#4-deprecations
[v1.4]: https://github.com/elixir-lang/elixir/blob/v1.4/CHANGELOG.md#4-deprecations
[v1.5]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-deprecations
[v1.5]: https://github.com/elixir-lang/elixir/blob/v1.5/CHANGELOG.md#4-deprecations
+3 -3
View File
@@ -104,14 +104,14 @@ Other constructs are `for`, `with`, `try`/`rescue`/`catch`/`else`/, and the `mat
## Failing guards
Errors in guards do not result in a runtime error, but in the erroring guard fail. For example, the `length/1` function only works with lists, and if we use it on anything else it fails:
Errors in guards do not result in runtime errors, but in guards failing. For example, the `length/1` function only works with lists. If we use it with anything else, a runtime error is raised:
```elixir
iex> length("hello")
** (ArgumentError) argument error
```
However, when used in guards, it simply makes the corresponding clause fail (i.e., not match):
However, when used in guards, the corresponding clause simply fails to match:
```elixir
iex> case "hello" do
@@ -123,7 +123,7 @@ iex> case "hello" do
:length_failed
```
In many cases, we can take advantage of this: in the code above, for example, we can use `length/1` to both check that the given thing is a list *and* check some properties of its length (instead of using `is_list(something) and length(something) > 0`).
In many cases, we can take advantage of this. In the code above, we used `length/1` to both check that the given thing is a list *and* check some properties of its length (instead of using `is_list(something) and length(something) > 0`).
## Expressions in guard clauses
+7 -1
View File
@@ -183,8 +183,14 @@ env_for_eval(Env, Opts) ->
false -> nil
end,
FA = case lists:keyfind(function, 1, Opts) of
{function, {Function, Arity}} when is_atom(Function), is_integer(Arity) -> {Function, Arity};
{function, nil} -> nil;
false -> nil
end,
Env#{
file := File, module := Module,
file := File, module := Module, function := FA,
macros := Macros, functions := Functions,
requires := Requires, aliases := Aliases, line := Line
}.
+1 -3
View File
@@ -5,15 +5,13 @@
-define(var_context, ?MODULE).
-record(elixir_erl, {
def=nil, %% a tuple with the current definition {def | ..., name, arity}
context=nil, %% can be match, guards or nil
extra=nil, %% extra information about the context, like pin_guard and map_key
caller=false, %% when true, it means caller was invoked
vars=#{}, %% a map of defined variables and their alias
backup_vars=nil, %% a copy of vars to be used on ^var
match_vars=nil, %% a set of all variables defined in a particular match
export_vars=nil, %% a dict of all variables defined in a particular clause
extra_guards=nil, %% extra guards from args expansion
extra_guards=[], %% extra guards from args expansion
counter=#{}, %% a map counting the variables defined
file=(<<"nofile">>) %% the current scope filename
}).
+72 -7
View File
@@ -2,20 +2,20 @@
%% fn, receive and friends. try is handled in elixir_try.
-module(elixir_clauses).
-export([match/3, clause/5, def/2, head/2,
'case'/3, 'receive'/3, 'try'/3, 'cond'/3,
'case'/3, 'receive'/3, 'try'/3, 'cond'/3, with/3,
format_error/1]).
-import(elixir_errors, [form_error/4]).
-include("elixir.hrl").
match(Fun, Expr, #{context := match} = E) ->
Fun(Expr, E);
match(Fun, Expr, #{context := Context, prematch_vars := nil, vars := Vars} = E) ->
{EExpr, EE} = Fun(Expr, E#{context := match, prematch_vars := Vars}),
{EExpr, EE#{context := Context, prematch_vars := nil}}.
match(Fun, Expr, #{context := Context, match_vars := Match, prematch_vars := nil, vars := Vars} = E) ->
{EExpr, EE} = Fun(Expr, E#{context := match, match_vars := [], prematch_vars := Vars}),
{EExpr, EE#{context := Context, match_vars := Match, prematch_vars := nil}}.
def({Meta, Args, Guards, Body}, E) ->
{EArgs, EA} = elixir_expand:expand(Args, E#{context := match}),
{EGuards, EG} = guard(Guards, EA#{context := guard}),
def({Meta, Args, Guards, Body}, #{match_vars := Match} = E) ->
{EArgs, EA} = elixir_expand:expand(Args, E#{context := match, match_vars := []}),
{EGuards, EG} = guard(Guards, EA#{context := guard, match_vars := Match}),
{EBody, _} = elixir_expand:expand(Body, EG#{context := ?key(E, context)}),
{Meta, EArgs, EGuards, EBody}.
@@ -112,6 +112,66 @@ expand_receive(Meta, {'after', _}, _Acc, E) ->
expand_receive(Meta, {Key, _}, _Acc, E) ->
form_error(Meta, ?key(E, file), ?MODULE, {unexpected_option, 'receive', Key}).
%% With
with(Meta, Args, E) ->
{Exprs, Opts0} =
case elixir_utils:split_last(Args) of
{_, LastArg} = SplitResult when is_list(LastArg) ->
SplitResult;
_ ->
{Args, []}
end,
{EExprs, {EE, HasMatch}} = lists:mapfoldl(fun expand_with/2, {E, false}, Exprs),
{EDo, Opts1} = expand_with_do(Meta, Opts0, EE),
{EOpts, Opts2} = expand_with_else(Meta, Opts1, E, HasMatch),
case Opts2 of
[{Key, _} | _] ->
form_error(Meta, ?key(E, file), elixir_clauses, {unexpected_option, with, Key});
[] ->
ok
end,
{{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, E}.
expand_with({'<-', Meta, [{Name, _, Ctx}, _] = Args}, Acc) when is_atom(Name), is_atom(Ctx) ->
expand_with({'=', Meta, Args}, Acc);
expand_with({'<-', Meta, [Left, Right]}, {E, _HasMatch}) ->
{ERight, ER} = elixir_expand:expand(Right, E),
{[ELeft], EL} = head([Left], E),
{{'<-', Meta, [ELeft, ERight]}, {elixir_env:mergev(EL, ER), true}};
expand_with(Expr, {E, HasMatch}) ->
{EExpr, EE} = elixir_expand:expand(Expr, E),
{EExpr, {EE, HasMatch}}.
expand_with_do(Meta, Opts, E) ->
case lists:keytake(do, 1, Opts) of
{value, {do, Expr}, RestOpts} ->
{EExpr, _} = elixir_expand:expand(Expr, E),
{EExpr, RestOpts};
false ->
form_error(Meta, ?key(E, file), elixir_expand, {missing_option, 'with', [do]})
end.
expand_with_else(Meta, Opts, E, HasMatch) ->
case lists:keytake(else, 1, Opts) of
{value, Pair, RestOpts} ->
if
HasMatch ->
ok;
true ->
Message = "\"else\" clauses will never match because all patterns in \"with\" will always match",
elixir_errors:warn(?line(Meta), ?key(E, file), Message)
end,
Fun = expand_one(Meta, 'with', 'else', fun head/2),
EPair = expand_without_export(Meta, 'with', Fun, Pair, E),
{[EPair], RestOpts};
false ->
{[], Opts}
end.
%% Try
'try'(Meta, [], E) ->
@@ -168,6 +228,11 @@ expand_rescue(Meta, _, E) ->
expand_rescue({Name, _, Atom} = Var, E) when is_atom(Name), is_atom(Atom) ->
match(fun elixir_expand:expand/2, Var, E);
%% rescue var in _ => rescue var
expand_rescue({in, _, [{Name, _, VarContext} = Var, {'_', _, UnderscoreContext}]}, E)
when is_atom(Name), is_atom(VarContext), is_atom(UnderscoreContext) ->
expand_rescue(Var, E);
%% rescue var in [Exprs]
expand_rescue({in, Meta, [Left, Right]}, E) ->
{ELeft, EL} = match(fun elixir_expand:expand/2, Left, E),
+2 -2
View File
@@ -22,7 +22,7 @@ local_for(Module, Name, Arity, Kinds) ->
of
{[{_, Kind, Meta, File, _, _}], Clauses} ->
case (Kinds == all) orelse (lists:member(Kind, Kinds)) of
true -> elixir_erl:definition_to_anonymous(File, Module, Tuple, Kind, Meta,
true -> elixir_erl:definition_to_anonymous(File, Module, Kind, Meta,
[Clause || {_, Clause} <- Clauses]);
false -> false
end;
@@ -212,7 +212,7 @@ unpack_defaults(Kind, Meta, Name, Args, E) ->
unpack_defaults(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, Acc, Clauses) ->
Base = match_defaults(Acc, length(Acc), []),
{Args, Invoke} = extract_defaults(List, length(Base), [], []),
Clause = {Meta, Base ++ Args, [], {super, DefaultMeta, Base ++ Invoke}},
Clause = {Meta, Base ++ Args, [], {super, DefaultMeta, [{Kind, Name} | Base] ++ Invoke}},
unpack_defaults(Kind, Meta, Name, T, [Expr | Acc], [Clause | Clauses]);
unpack_defaults(Kind, Meta, Name, [H | T], Acc, Clauses) ->
unpack_defaults(Kind, Meta, Name, T, [H | Acc], Clauses);
+5 -2
View File
@@ -19,10 +19,13 @@ new() ->
lexical_tracker => nil, %% holds the lexical tracker PID
vars => [], %% a set of defined variables
export_vars => nil, %% a set of variables to be exported in some constructs
prematch_vars => nil}. %% a set of variables defined before the current match
prematch_vars => nil, %% a set of variables defined before the current match
match_vars => warn}. %% handling of new variables
linify({Line, Env}) ->
Env#{line := Line}.
Env#{line := Line};
linify(#{} = Env) ->
Env.
env_to_scope(#{file := File, context := Context}) ->
#elixir_erl{file=File, context=Context}.
+52 -46
View File
@@ -1,8 +1,8 @@
%% Compiler backend to Erlang.
-module(elixir_erl).
-export([elixir_to_erl/1, definition_to_anonymous/6, compile/1,
-export([elixir_to_erl/1, definition_to_anonymous/5, compile/1,
get_ann/1, remote/4, add_beam_chunks/2, debug_info/4,
definition_scope/5]).
definition_scope/2]).
-include("elixir.hrl").
%% TODO: Remove extra chunk functionality when OTP 20+.
@@ -61,8 +61,8 @@ remote(Ann, Module, Function, Args) when is_atom(Module), is_atom(Function), is_
%% Converts an Elixir definition to an anonymous function.
definition_to_anonymous(File, Module, {Name, Arity}, Kind, Meta, Clauses) ->
ErlClauses = [translate_clause(Kind, Name, Arity, Clause, File) || Clause <- Clauses],
definition_to_anonymous(File, Module, Kind, Meta, Clauses) ->
ErlClauses = [translate_clause(Kind, Clause, File) || Clause <- Clauses],
Fun = {'fun', ?ann(Meta), {clauses, ErlClauses}},
LocalHandler = fun(LocalName, LocalArgs) -> invoke_local(Module, LocalName, LocalArgs) end,
{value, Result, _Binding} = erl_eval:expr(Fun, [], {value, LocalHandler}),
@@ -137,14 +137,14 @@ elixir_to_erl_cons2([], Acc) ->
%% Returns a definition scope for translation.
definition_scope(Meta, Kind, Name, Arity, File) ->
definition_scope(Meta, File) ->
%% TODO: We only need to do this dance because some
%% warnings are raised in elixir_erl_pass. Once we remove
%% all warnings from the Erlang pass, we can remove the
%% file field from #elixir_erl and clean up the code.
case lists:keyfind(location, 1, Meta) of
{location, {F, _}} -> #elixir_erl{def = {Kind, Name, Arity}, file = F};
false -> #elixir_erl{def = {Kind, Name, Arity}, file = File}
{location, {F, _}} -> #elixir_erl{file=F};
false -> #elixir_erl{file=File}
end.
%% Compilation hook.
@@ -159,37 +159,57 @@ compile(#{module := Module} = Map) ->
end,
load_form(Map, Data, Prefix, Forms, Specs).
dynamic_form(#{module := Module, line := Line, file := File, attributes := Attributes,
definitions := Definitions, unreachable := Unreachable}) ->
{Def, Defmacro, Macros, Exports, Functions} =
split_definition(Definitions, File, Unreachable, [], [], [], [], {[], []}),
Location = {elixir_utils:characters_to_list(elixir_utils:relative_to_cwd(File)), Line},
Prefix = [{attribute, Line, file, Location},
{attribute, Line, module, Module},
{attribute, Line, compile, no_auto_import}],
Forms0 = functions_form(Line, Module, Def, Defmacro, Exports, Functions),
Forms1 = attributes_form(Line, Attributes, Forms0),
{Prefix, Forms1, Macros, Unreachable}.
% Definitions
split_definition([{Tuple, def, Meta, Clauses} | T], File, Unreachable,
Def, Defmacro, Exports, Functions) ->
{_, _, N, A, _} = Function = translate_definition(def, Meta, File, Tuple, Clauses),
split_definition(T, File, Unreachable, [Tuple | Def], Defmacro, [{N, A} | Exports],
add_definition(Meta, Function, Functions));
split_definition([{Tuple, defp, Meta, Clauses} | T], File, Unreachable,
Def, Defmacro, Exports, Functions) ->
Function = translate_definition(defp, Meta, File, Tuple, Clauses),
split_definition([{Tuple, Kind, Meta, Clauses} | T], File, Unreachable,
Def, Defmacro, Macros, Exports, Functions) ->
case lists:member(Tuple, Unreachable) of
false ->
split_definition(T, File, Unreachable, Def, Defmacro, Exports,
add_definition(Meta, Function, Functions));
split_definition(Tuple, Kind, Meta, Clauses, T, File, Unreachable,
Def, Defmacro, Macros, Exports, Functions);
true ->
split_definition(T, File, Unreachable, Def, Defmacro, Exports, Functions)
split_definition(T, File, Unreachable, Def, Defmacro, Macros, Exports, Functions)
end;
split_definition([], _File, _Unreachable, Def, Defmacro, Macros, Exports, {Head, Tail}) ->
{Def, Defmacro, Macros, Exports, Head ++ Tail}.
split_definition([{Tuple, defmacro, Meta, Clauses} | T], File, Unreachable,
Def, Defmacro, Exports, Functions) ->
{_, _, N, A, _} = Function = translate_definition(defmacro, Meta, File, Tuple, Clauses),
split_definition(T, File, Unreachable, Def, [Tuple | Defmacro], [{N, A} | Exports],
add_definition(Meta, Function, Functions));
split_definition(Tuple, def, Meta, Clauses, T, File, Unreachable,
Def, Defmacro, Macros, Exports, Functions) ->
{_, _, N, A, _} = Entry = translate_definition(def, Meta, File, Tuple, Clauses),
split_definition(T, File, Unreachable, [Tuple | Def], Defmacro, Macros, [{N, A} | Exports],
add_definition(Meta, Entry, Functions));
split_definition([{_, defmacrop, _Meta, _Clauses} | T], File, Unreachable,
Def, Defmacro, Exports, Functions) ->
split_definition(T, File, Unreachable, Def, Defmacro, Exports, Functions);
split_definition(Tuple, defp, Meta, Clauses, T, File, Unreachable,
Def, Defmacro, Macros, Exports, Functions) ->
Entry = translate_definition(defp, Meta, File, Tuple, Clauses),
split_definition(T, File, Unreachable, Def, Defmacro, Macros, Exports,
add_definition(Meta, Entry, Functions));
split_definition([], _File, _Unreachable, Def, Defmacro, Exports, {Head, Tail}) ->
{Def, Defmacro, Exports, Head ++ Tail}.
split_definition(Tuple, defmacro, Meta, Clauses, T, File, Unreachable,
Def, Defmacro, Macros, Exports, Functions) ->
{_, _, N, A, _} = Entry = translate_definition(defmacro, Meta, File, Tuple, Clauses),
split_definition(T, File, Unreachable, Def, [Tuple | Defmacro], [Tuple | Macros], [{N, A} | Exports],
add_definition(Meta, Entry, Functions));
split_definition(Tuple, defmacrop, Meta, Clauses, T, File, Unreachable,
Def, Defmacro, Macros, Exports, Functions) ->
Entry = translate_definition(defmacro, Meta, File, Tuple, Clauses),
split_definition(T, File, Unreachable, Def, Defmacro, [Tuple | Macros], Exports,
add_definition(Meta, Entry, Functions)).
add_definition(Meta, Body, {Head, Tail}) ->
case lists:keyfind(location, 1, Meta) of
@@ -206,15 +226,15 @@ add_definition(Meta, Body, {Head, Tail}) ->
end.
translate_definition(Kind, Meta, File, {Name, Arity}, Clauses) ->
ErlClauses = [translate_clause(Kind, Name, Arity, Clause, File) || Clause <- Clauses],
ErlClauses = [translate_clause(Kind, Clause, File) || Clause <- Clauses],
case is_macro(Kind) of
true -> {function, ?ann(Meta), elixir_utils:macro_name(Name), Arity + 1, ErlClauses};
false -> {function, ?ann(Meta), Name, Arity, ErlClauses}
end.
translate_clause(Kind, Name, Arity, {Meta, Args, Guards, Body}, File) ->
S = definition_scope(Meta, Kind, Name, Arity, File),
translate_clause(Kind, {Meta, Args, Guards, Body}, File) ->
S = definition_scope(Meta, File),
{TClause, TS} = elixir_erl_clauses:clause(Meta,
fun elixir_erl_pass:translate_args/2, Args, Body, Guards, S),
@@ -245,20 +265,6 @@ is_macro(_) -> false.
% Functions
dynamic_form(#{module := Module, line := Line, file := File, attributes := Attributes,
definitions := Definitions, unreachable := Unreachable}) ->
{Def, Defmacro, Exports, Functions} =
split_definition(Definitions, File, Unreachable, [], [], [], {[], []}),
Location = {elixir_utils:characters_to_list(elixir_utils:relative_to_cwd(File)), Line},
Prefix = [{attribute, Line, file, Location},
{attribute, Line, module, Module},
{attribute, Line, compile, no_auto_import}],
Forms0 = functions_form(Line, Module, Def, Defmacro, Exports, Functions),
Forms1 = attributes_form(Line, Attributes, Forms0),
{Prefix, Forms1, Defmacro, Unreachable}.
functions_form(Line, Module, Def, Defmacro, Exports, Body) ->
{Spec, Info} = add_info_function(Line, Module, Def, Defmacro),
[{attribute, Line, export, lists:sort([{'__info__', 1} | Exports])}, Spec, Info | Body].
+10 -20
View File
@@ -1,49 +1,39 @@
%% Handle code related to args, guard and -> matching for case,
%% fn, receive and friends. try is handled in elixir_erl_try.
-module(elixir_erl_clauses).
-export([match/3, clause/6, clauses/3, guards/3, get_clauses/3, get_clauses/4]).
-export([match/3, clause/6, clauses/3, guards/3, get_clauses/3]).
-include("elixir.hrl").
%% Get clauses under the given key.
get_clauses(Key, Keyword, As) ->
get_clauses(Key, Keyword, As, false).
get_clauses(Key, Keyword, As, AllowNil) ->
case lists:keyfind(Key, 1, Keyword) of
{Key, Clauses} when is_list(Clauses) ->
[{As, Meta, Left, Right} || {'->', Meta, [Left, Right]} <- Clauses];
{Key, nil} when AllowNil ->
[];
false ->
_ ->
[]
end.
%% Translate matches
match(Fun, Args, #elixir_erl{context=Context, match_vars=MatchVars,
backup_vars=BackupVars, vars=Vars} = S) when Context /= match ->
{Result, NewS} = match(Fun, Args, S#elixir_erl{context=match,
match_vars=#{}, backup_vars=Vars}),
{Result, NewS#elixir_erl{context=Context,
match_vars=MatchVars, backup_vars=BackupVars}};
match(Fun, Args, #elixir_erl{context=Context, backup_vars=BackupVars, vars=Vars} = S) when Context /= match ->
{Result, NewS} = match(Fun, Args, S#elixir_erl{context=match, backup_vars=Vars}),
{Result, NewS#elixir_erl{context=Context, backup_vars=BackupVars}};
match(Fun, Args, S) -> Fun(Args, S).
%% Translate clauses with args, guards and expressions
clause(Meta, Fun, Args, Expr, Guards, S) when is_list(Meta) ->
{TArgs, SA} = match(Fun, Args, S#elixir_erl{extra_guards=[]}),
{TExpr, SE} = elixir_erl_pass:translate(Expr,
SA#elixir_erl{extra_guards=nil, export_vars=S#elixir_erl.export_vars}),
Extra = SA#elixir_erl.extra_guards,
TGuards = guards(Guards, Extra, SA),
{TArgs, SA} = match(Fun, Args, S),
SG = SA#elixir_erl{extra_guards=[]},
TGuards = guards(Guards, SA#elixir_erl.extra_guards, SG),
{TExpr, SE} = elixir_erl_pass:translate(Expr, SG#elixir_erl{export_vars=S#elixir_erl.export_vars}),
{{clause, ?ann(Meta), TArgs, TGuards, unblock(TExpr)}, SE}.
% Translate/Extract guards from the given expression.
guards(Guards, Extra, S) ->
SG = S#elixir_erl{context=guard, extra_guards=nil},
SG = S#elixir_erl{context=guard},
case Guards of
[] -> case Extra of [] -> []; _ -> [Extra] end;
_ -> [translate_guard(Guard, Extra, SG) || Guard <- Guards]
+2 -2
View File
@@ -50,11 +50,11 @@ translate_gen(_Meta, Left, Right, T, S) ->
{TRight, SR} = elixir_erl_pass:translate(Right, S),
{LeftArgs, LeftGuards} = elixir_utils:extract_guards(Left),
{TLeft, SL} = elixir_erl_clauses:match(fun elixir_erl_pass:translate/2, LeftArgs,
SR#elixir_erl{extra=pin_guard, extra_guards=[]}),
SR#elixir_erl{extra=pin_guard}),
TLeftGuards = elixir_erl_clauses:guards(LeftGuards, [], SL),
ExtraGuards = [{nil, X} || X <- SL#elixir_erl.extra_guards],
SF = SL#elixir_erl{extra=S#elixir_erl.extra, extra_guards=nil},
SF = SL#elixir_erl{extra=S#elixir_erl.extra, extra_guards=[]},
{TT, {TFilters, TS}} = translate_filters(T, SF),
+60 -18
View File
@@ -41,15 +41,16 @@ translate({'__block__', Meta, Args}, S) when is_list(Args) ->
translate({'__CALLER__', Meta, Atom}, S) when is_atom(Atom) ->
{{var, ?ann(Meta), '__CALLER__'}, S#elixir_erl{caller=true}};
translate({'super', Meta, Args}, #elixir_erl{def={Kind, Name, _}} = S) ->
translate({'super', Meta, [{Kind, Name} | Args]}, S) ->
%% In the expanded AST, super is used to invoke a function
%% with the same name but possibly different arity.
%% in the current module originated from a default clause
%% or a super call.
{TArgs, SA} = translate_args(Args, S),
Ann = ?ann(Meta),
if
Kind == defmacro; Kind == defmacrop ->
MacroName = elixir_utils:macro_name(Name),
{{call, Ann, {atom, Ann, MacroName}, [{var, Ann, '_@CALLER'} | TArgs]}, SA};
{{call, Ann, {atom, Ann, MacroName}, [{var, Ann, '__CALLER__'} | TArgs]}, SA#elixir_erl{caller=true}};
Kind == def; Kind == defp ->
{{call, Ann, {atom, Ann, Name}, TArgs}, SA}
end;
@@ -70,8 +71,8 @@ translate({'&', Meta, [{'/', _, [{Fun, _, Atom}, Arity]}]}, S)
translate({fn, Meta, Clauses}, S) ->
Transformer = fun({'->', CMeta, [ArgsWithGuards, Expr]}, Acc) ->
{Args, Guards} = elixir_utils:extract_splat_guards(ArgsWithGuards),
{TClause, TS } = elixir_erl_clauses:clause(CMeta, fun translate_fn_match/2,
Args, Expr, Guards, Acc),
{TClause, TS} = elixir_erl_clauses:clause(CMeta, fun translate_fn_match/2,
Args, Expr, Guards, Acc),
{TClause, elixir_erl_var:mergec(S, TS)}
end,
{TClauses, NS} = lists:mapfoldl(Transformer, S, Clauses),
@@ -101,29 +102,28 @@ translate({'case', Meta, [Expr, Opts]}, S) ->
%% Try
translate({'try', Meta, [Opts]}, S) ->
SN = S#elixir_erl{extra=nil},
Do = proplists:get_value('do', Opts, nil),
{TDo, SB} = translate(Do, SN),
{TDo, SB} = translate(Do, S),
Catch = [Tuple || {X, _} = Tuple <- Opts, X == 'rescue' orelse X == 'catch'],
{TCatch, SC} = elixir_erl_try:clauses(Meta, Catch, mergec(SN, SB)),
{TCatch, SC} = elixir_erl_try:clauses(Meta, Catch, mergec(S, SB)),
{TAfter, SA} = case lists:keyfind('after', 1, Opts) of
{'after', After} ->
{TBlock, SAExtracted} = translate(After, mergec(SN, SC)),
{TBlock, SAExtracted} = translate(After, mergec(S, SC)),
{unblock(TBlock), SAExtracted};
false ->
{[], mergec(SN, SC)}
{[], mergec(S, SC)}
end,
Else = elixir_erl_clauses:get_clauses(else, Opts, match),
{TElse, SE} = elixir_erl_clauses:clauses(Meta, Else, mergec(SN, SA)),
{TElse, SE} = elixir_erl_clauses:clauses(Meta, Else, mergec(S, SA)),
{{'try', ?ann(Meta), unblock(TDo), TElse, TCatch, TAfter}, mergec(S, SE)};
%% Receive
translate({'receive', Meta, [Opts]}, S) ->
Do = elixir_erl_clauses:get_clauses(do, Opts, match, true),
Do = elixir_erl_clauses:get_clauses(do, Opts, match),
case lists:keyfind('after', 1, Opts) of
false ->
@@ -137,17 +137,22 @@ translate({'receive', Meta, [Opts]}, S) ->
{{'receive', ?ann(Meta), FClauses, FExpr, FAfter}, SC}
end;
%% Comprehensions
%% Comprehensions and with
translate({for, Meta, [_ | _] = Args}, S) ->
elixir_erl_for:translate(Meta, Args, true, S);
translate({with, Meta, [_ | _] = Args}, S) ->
{Exprs, [{do, Do} | Opts]} = elixir_utils:split_last(Args),
{ElseClause, SE} = translate_with_else(Meta, Opts, S),
{With, SD} = translate_with_do(Exprs, Do, ElseClause, elixir_erl_var:mergec(S, SE)),
{With, elixir_erl_var:mergec(S, SD)};
%% Variables
translate({'^', Meta, [{Name, VarMeta, Kind}]}, #elixir_erl{context=match, file=File} = S) when is_atom(Name), is_atom(Kind) ->
Tuple = {Name, var_kind(VarMeta, Kind)},
{ok, {Value, _Counter, Safe}} = maps:find(Tuple, S#elixir_erl.backup_vars),
elixir_erl_var:warn_underscored_var_access(VarMeta, File, Name),
elixir_erl_var:warn_unsafe_var(VarMeta, File, Name, Safe),
PAnn = ?ann(Meta),
@@ -235,10 +240,10 @@ translate(Other, S) ->
translate_case(true, Meta, Expr, Opts, S) ->
Clauses = elixir_erl_clauses:get_clauses(do, Opts, match),
{TExpr, SE} = translate(Expr, S),
{TClauses, SC} = elixir_erl_clauses:clauses(Meta, Clauses, SE#elixir_erl{extra=nil}),
{{'case', ?ann(Meta), TExpr, TClauses}, SC#elixir_erl{extra=SE#elixir_erl.extra}};
{TClauses, SC} = elixir_erl_clauses:clauses(Meta, Clauses, SE),
{{'case', ?ann(Meta), TExpr, TClauses}, SC};
translate_case(false, Meta, Expr, Opts, S) ->
{Case, SC} = translate_case(true, Meta, Expr, Opts, S#elixir_erl{extra=nil}),
{Case, SC} = translate_case(true, Meta, Expr, Opts, S),
{Case, elixir_erl_var:mergec(S, SC)}.
translate_list([{'|', _, [_, _]=Args}], Fun, Acc, List) ->
@@ -350,6 +355,43 @@ returns_boolean(Condition, Body) ->
false -> false
end.
%% with
translate_with_else(Meta, [], S) ->
Ann = ?ann(?generated(Meta)),
{VarName, _, SC} = elixir_erl_var:build('_', S),
Var = {var, Ann, VarName},
{{clause, Ann, [Var], [], [Var]}, SC};
translate_with_else(Meta, [{else, Else}], S) ->
Generated = ?generated(Meta),
ElseVarEx = {else, Generated, ?var_context},
{ElseVarErl, SV} = elixir_erl_var:assign(Generated, else, ?var_context, S),
RaiseVar = {catch_all, Generated, ?var_context},
RaiseExpr = {{'.', Generated, [erlang, error]}, Generated, [{with_clause, RaiseVar}]},
RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]},
Case = {'case', [{export_vars, false} | Meta], [ElseVarEx, [{do, Else ++ [RaiseClause]}]]},
{TranslatedCase, SC} = elixir_erl_pass:translate(Case, SV),
{{clause, ?ann(Generated), [ElseVarErl], [], [TranslatedCase]}, SC}.
translate_with_do([{'<-', Meta, [Left, Expr]} | Rest], Do, Else, S) ->
{Args, Guards} = elixir_utils:extract_guards(Left),
{TExpr, SR} = elixir_erl_pass:translate(Expr, S),
{TArgs, SA} = elixir_erl_clauses:match(fun elixir_erl_pass:translate/2, Args, SR),
TGuards = elixir_erl_clauses:guards(Guards, [], SA),
{TBody, SB} = translate_with_do(Rest, Do, Else, SA),
Ann = ?ann(?generated(Meta)),
Clause = {clause, Ann, [TArgs], TGuards, unblock(TBody)},
{{'case', Ann, TExpr, [Clause, Else]}, SB};
translate_with_do([Expr | Rest], Do, Else, S) ->
{TExpr, TS} = elixir_erl_pass:translate(Expr, S),
{TRest, RS} = translate_with_do(Rest, Do, Else, TS),
{{block, 0, [TExpr | unblock(TRest)]}, RS};
translate_with_do([], Do, _Else, S) ->
elixir_erl_pass:translate(Do, S).
%% Maps and structs
translate_map(Meta, [{'|', _Meta, [Update, Assocs]}], S) ->
@@ -513,7 +555,7 @@ translate_remote(Left, Right, Meta, Args, S) ->
{{call, Ann, {remote, Ann, TLeft, TRight}, TArgs}, SC}
end.
is_always_string({{'.', _, [Module, Function]}, Args}) ->
is_always_string({{'.', _, [Module, Function]}, _, Args}) ->
is_always_string(Module, Function, length(Args));
%% Binary literals were already excluded in earlier passes.
is_always_string(_Ast) ->
+12 -10
View File
@@ -23,19 +23,21 @@ each_clause({'catch', Meta, Raw, Expr}, S) ->
Condition = [{'{}', Meta, Final}],
{TC, TS} = elixir_erl_clauses:clause(Meta, fun elixir_erl_pass:translate_args/2,
Condition, Expr, Guards, S),
Condition, Expr, Guards, S),
{[TC], TS};
each_clause({rescue, Meta, [{in, _, [Left, Right]}], Expr}, S) ->
{VarName, _, CS} = elixir_erl_var:build('_', S),
Var = {VarName, Meta, nil},
{Parts, Safe, FS} = rescue_guards(Meta, Var, Right, CS),
Body = rescue_clause_body(Left, Expr, Safe, Var, Meta),
{TempName, _, CS} = elixir_erl_var:build('_', S),
TempVar = {TempName, Meta, ?var_context},
{Parts, Safe, FS} = rescue_guards(Meta, TempVar, Right, CS),
Body = rescue_clause_body(Left, Expr, Safe, TempVar, Meta),
build_rescue(Meta, Parts, Body, FS);
each_clause({rescue, Meta, [{VarName, _, Atom} = Var], Expr}, S) when is_atom(VarName), is_atom(Atom) ->
Body = rescue_clause_body(Var, Expr, false, Var, Meta),
build_rescue(Meta, _Parts = [{Var, []}], Body, S).
each_clause({rescue, Meta, [{VarName, _, Context} = Left], Expr}, S) when is_atom(VarName), is_atom(Context) ->
{TempName, _, CS} = elixir_erl_var:build('_', S),
TempVar = {TempName, Meta, ?var_context},
Body = rescue_clause_body(Left, Expr, false, TempVar, Meta),
build_rescue(Meta, [{TempVar, []}], Body, CS).
rescue_clause_body({'_', _, Atom}, Expr, _Safe, _Var, _Meta) when is_atom(Atom) ->
Expr;
@@ -54,7 +56,7 @@ build_rescue(Meta, Parts, Body, S) ->
{{clause, Line, TMatches, _, TBody}, TS} =
elixir_erl_clauses:clause(Meta, fun elixir_erl_pass:translate_args/2,
Matches, Body, [], S),
Matches, Body, [], S),
TClauses =
[begin
@@ -74,7 +76,7 @@ rescue_guards(Meta, Var, Aliases, S) ->
[] -> {[], S};
_ ->
{VarName, _, CS} = elixir_erl_var:build('_', S),
StructVar = {VarName, Meta, nil},
StructVar = {VarName, Meta, 'Elixir'},
Map = {'%{}', Meta, [{'__struct__', StructVar}, {'__exception__', true}]},
Match = {'=', Meta, [Map, Var]},
Guards = [{erl(Meta, '=='), Meta, [StructVar, Mod]} || Mod <- Elixir],
+47 -85
View File
@@ -1,64 +1,68 @@
%% Convenience functions used to manipulate scope and its variables.
-module(elixir_erl_var).
-export([translate/4, build/2, context_info/1,
-export([translate/4, build/2, assign/4,
load_binding/2, dump_binding/2,
mergev/2, mergec/2, merge_vars/2, merge_opt_vars/2,
warn_unsafe_var/4, warn_underscored_var_access/3, format_error/1
warn_unsafe_var/4, format_error/1
]).
-include("elixir.hrl").
%% VAR HANDLING
translate(Meta, Name, Kind, S) when is_atom(Kind); is_integer(Kind) ->
Ann = ?ann(Meta),
Tuple = {Name, Kind},
Vars = S#elixir_erl.vars,
{Current, Exists, Safe} =
case maps:find({Name, Kind}, Vars) of
{ok, {VarC, _, VarS}} -> {VarC, true, VarS};
error -> {nil, false, true}
{Current, Safe} =
case maps:find(Tuple, S#elixir_erl.vars) of
{ok, {VarC, _, VarS}} -> {VarC, VarS};
error -> {nil, true}
end,
case S#elixir_erl.context of
match ->
MatchVars = S#elixir_erl.match_vars,
Previous =
case maps:find(Tuple, S#elixir_erl.backup_vars) of
{ok, {BackupVarC, _, _}} -> BackupVarC;
error -> nil
end,
case Exists andalso maps:get(Tuple, MatchVars, false) of
if
Current /= nil, Current /= Previous ->
{{var, ?ann(Meta), Current}, S};
true ->
warn_underscored_var_repeat(Meta, S#elixir_erl.file, Name, Kind),
{{var, Ann, Current}, S};
false ->
%% We attempt to give vars a nice name because we
%% still use the unused vars warnings from erl_lint.
%%
%% Once we move the warning to Elixir compiler, we
%% can name vars as _@COUNTER.
{NewVar, Counter, NS} =
if
Kind /= nil ->
build('_', S);
true ->
build(Name, S)
end,
FS = NS#elixir_erl{
vars=maps:put(Tuple, {NewVar, Counter, true}, Vars),
match_vars=maps:put(Tuple, true, MatchVars),
export_vars=case S#elixir_erl.export_vars of
nil -> nil;
EV -> maps:put(Tuple, {NewVar, Counter, true}, EV)
end
},
{{var, Ann, NewVar}, FS}
assign(Meta, Name, Kind, S)
end;
_ when Exists ->
warn_underscored_var_access(Meta, S#elixir_erl.file, Name),
_ when Current /= nil ->
warn_unsafe_var(Meta, S#elixir_erl.file, Name, Safe),
{{var, Ann, Current}, S}
{{var, ?ann(Meta), Current}, S}
end.
assign(Meta, Name, Kind, S) ->
Tuple = {Name, Kind},
%% We attempt to give vars a nice name because we
%% still use the unused vars warnings from erl_lint.
%%
%% Once we move the warning to Elixir compiler, we
%% can name vars as _@COUNTER.
{NewVar, Counter, NS} =
if
Kind /= nil ->
build('_', S);
true ->
build(Name, S)
end,
FS = NS#elixir_erl{
vars=maps:put(Tuple, {NewVar, Counter, true}, S#elixir_erl.vars),
export_vars=case S#elixir_erl.export_vars of
nil -> nil;
EV -> maps:put(Tuple, {NewVar, Counter, true}, EV)
end
},
{{var, ?ann(Meta), NewVar}, FS}.
build(Key, #elixir_erl{counter=Counter} = S) ->
Cnt =
case maps:find(Key, Counter) of
@@ -69,43 +73,14 @@ build(Key, #elixir_erl{counter=Counter} = S) ->
Cnt,
S#elixir_erl{counter=maps:put(Key, Cnt, Counter)}}.
context_info(Kind) when Kind == nil; is_integer(Kind) -> "";
context_info(Kind) -> io_lib:format(" (context ~ts)", [elixir_aliases:inspect(Kind)]).
warn_underscored_var_repeat(Meta, File, Name, Kind) ->
Warn = should_warn(Meta),
case atom_to_list(Name) of
"_@" ++ _ ->
ok; %% Automatically generated variables
"_" ++ _ when Warn ->
elixir_errors:form_warn(Meta, File, ?MODULE, {unused_match, Name, Kind});
_ ->
ok
end.
warn_unsafe_var(Meta, File, Name, Safe) ->
Warn = should_warn(Meta),
if
(not Safe) and Warn ->
elixir_errors:form_warn(Meta, File, ?MODULE, {unsafe_var, Name});
case (not Safe) andalso (lists:keyfind(generated, 1, Meta) /= {generated, true}) of
true ->
elixir_errors:form_warn(Meta, File, ?MODULE, {unsafe_var, Name});
false ->
ok
end.
warn_underscored_var_access(Meta, File, Name) ->
Warn = should_warn(Meta),
case atom_to_list(Name) of
"_@" ++ _ ->
ok; %% Automatically generated variables
"_" ++ _ when Warn ->
elixir_errors:form_warn(Meta, File, ?MODULE, {underscore_var_access, Name});
_ ->
ok
end.
should_warn(Meta) ->
lists:keyfind(generated, 1, Meta) /= {generated, true}.
%% SCOPE MERGING
%% Receives two scopes and return a new scope based on
@@ -193,13 +168,6 @@ dump_binding(Binding, #elixir_erl{vars=Vars}) ->
%% Errors
format_error({unused_match, Name, Kind}) ->
io_lib:format("the underscored variable \"~ts\"~ts appears more than once in a "
"match. This means the pattern will only match if all \"~ts\" bind "
"to the same value. If this is the intended behaviour, please "
"remove the leading underscore from the variable name, otherwise "
"give the variables different names", [Name, context_info(Kind), Name]);
format_error({unsafe_var, Name}) ->
io_lib:format("the variable \"~ts\" is unsafe as it has been set inside "
"one of: case, cond, receive, if, and, or, &&, ||. "
@@ -214,10 +182,4 @@ format_error({unsafe_var, Name}) ->
" 1 -> :one\n"
" 2 -> :two\n"
" end\n\n"
"Unsafe variable found at:", [Name]);
format_error({underscore_var_access, Name}) ->
io_lib:format("the underscored variable \"~ts\" is used after being set. "
"A leading underscore indicates that the value of the variable "
"should be ignored. If this is intended please rename the "
"variable to remove the underscore", [Name]).
"Unsafe variable found at:", [Name]).
+99 -47
View File
@@ -6,7 +6,7 @@
%% =
expand({'=', Meta, [Left, Right]}, E) ->
assert_no_guard_scope(Meta, '=', E),
assert_no_guard_scope(Meta, "=", E),
{ERight, ER} = expand(Right, E),
{ELeft, EL} = elixir_clauses:match(fun expand/2, Left, E),
{{'=', Meta, [ELeft, ERight]}, elixir_env:mergev(EL, ER)};
@@ -62,7 +62,7 @@ expand({Kind, Meta, [{{'.', _, [Base, '{}']}, _, Refs} | Rest]}, E)
expand({alias, Meta, [Ref]}, E) ->
expand({alias, Meta, [Ref, []]}, E);
expand({alias, Meta, [Ref, Opts]}, E) ->
assert_no_match_or_guard_scope(Meta, alias, E),
assert_no_match_or_guard_scope(Meta, "alias", E),
{ERef, ER} = expand_without_aliases_report(Ref, E),
{EOpts, ET} = expand_opts(Meta, alias, [as, warn], no_alias_opts(Opts), ER),
@@ -76,7 +76,7 @@ expand({alias, Meta, [Ref, Opts]}, E) ->
expand({require, Meta, [Ref]}, E) ->
expand({require, Meta, [Ref, []]}, E);
expand({require, Meta, [Ref, Opts]}, E) ->
assert_no_match_or_guard_scope(Meta, require, E),
assert_no_match_or_guard_scope(Meta, "require", E),
{ERef, ER} = expand_without_aliases_report(Ref, E),
{EOpts, ET} = expand_opts(Meta, require, [as, warn], no_alias_opts(Opts), ER),
@@ -93,7 +93,7 @@ expand({import, Meta, [Left]}, E) ->
expand({import, Meta, [Left, []]}, E);
expand({import, Meta, [Ref, Opts]}, E) ->
assert_no_match_or_guard_scope(Meta, import, E),
assert_no_match_or_guard_scope(Meta, "import", E),
{ERef, ER} = expand_without_aliases_report(Ref, E),
{EOpts, ET} = expand_opts(Meta, import, [only, except, warn], Opts, ER),
@@ -209,7 +209,7 @@ expand({quote, Meta, [_, _]}, E) ->
%% Functions
expand({'&', Meta, [Arg]}, E) ->
assert_no_match_or_guard_scope(Meta, '&', E),
assert_no_match_or_guard_scope(Meta, "&", E),
case elixir_fn:capture(Meta, Arg, E) of
{remote, Remote, Fun, Arity} ->
is_atom(Remote) andalso
@@ -222,34 +222,36 @@ expand({'&', Meta, [Arg]}, E) ->
end;
expand({fn, Meta, Pairs}, E) ->
assert_no_match_or_guard_scope(Meta, fn, E),
assert_no_match_or_guard_scope(Meta, "fn", E),
elixir_fn:expand(Meta, Pairs, E);
%% Case/Receive/Try
expand({'cond', Meta, [Opts]}, E) ->
assert_no_match_or_guard_scope(Meta, 'cond', E),
assert_no_match_or_guard_scope(Meta, "cond", E),
assert_no_underscore_clause_in_cond(Opts, E),
{EClauses, EC} = elixir_clauses:'cond'(Meta, Opts, E),
{{'cond', Meta, [EClauses]}, EC};
expand({'case', Meta, [Expr, Options]}, Env) ->
expand({'case', Meta, [Expr, Options]}, E) ->
assert_no_match_or_guard_scope(Meta, "case", E),
ShouldExportVars = proplists:get_value(export_vars, Meta, true),
expand_case(ShouldExportVars, Meta, Expr, Options, Env);
expand_case(ShouldExportVars, Meta, Expr, Options, E);
expand({'receive', Meta, [Opts]}, E) ->
assert_no_match_or_guard_scope(Meta, 'receive', E),
assert_no_match_or_guard_scope(Meta, "receive", E),
{EClauses, EC} = elixir_clauses:'receive'(Meta, Opts, E),
{{'receive', Meta, [EClauses]}, EC};
expand({'try', Meta, [Opts]}, E) ->
assert_no_match_or_guard_scope(Meta, 'try', E),
assert_no_match_or_guard_scope(Meta, "try", E),
{EClauses, EC} = elixir_clauses:'try'(Meta, Opts, E),
{{'try', Meta, [EClauses]}, EC};
%% Comprehensions
expand({for, Meta, [_ | _] = Args}, E) ->
assert_no_match_or_guard_scope(Meta, "for", E),
{Cases, Block} =
case elixir_utils:split_last(Args) of
{OuterCases, OuterOpts} when is_list(OuterOpts) ->
@@ -281,25 +283,22 @@ expand({for, Meta, [_ | _] = Args}, E) ->
%% With
expand({with, Meta, [_ | _] = Args}, E) ->
elixir_with:expand(Meta, Args, E);
assert_no_match_or_guard_scope(Meta, "with", E),
elixir_clauses:with(Meta, Args, E);
%% Super
expand({super, Meta, Args}, #{file := File} = E) when is_list(Args) ->
assert_no_match_or_guard_scope(Meta, "super", E),
Module = assert_module_scope(Meta, super, E),
Function = assert_function_scope(Meta, super, E),
{_, Arity} = Function,
case length(Args) of
Arity ->
{OName, OArity} = elixir_overridable:super(Meta, File, Module, Function),
{Kind, Name} = elixir_overridable:super(Meta, File, Module, Function),
{EArgs, EA} = expand_args(Args, E),
OArgs =
if
OArity > Arity -> [{'__CALLER__', [], nil} | EArgs];
true -> EArgs
end,
{{OName, Meta, OArgs}, EA};
{{super, Meta, [{Kind, Name} | EArgs]}, EA};
_ ->
form_error(Meta, File, ?MODULE, wrong_number_of_args_for_super)
end;
@@ -315,8 +314,9 @@ expand({'^', Meta, [Arg]}, #{context := match, prematch_vars := PrematchVars} =
%% If the variable was defined, then we return the expanded ^, otherwise
%% we raise. We cannot use the expanded env because it would contain the
%% variable.
case lists:member({VarName, var_kind(VarMeta, Kind)}, PrematchVars) of
case lists:member({VarName, var_context(VarMeta, Kind)}, PrematchVars) of
true ->
warn_underscored_var_access(VarMeta, VarName, Kind, E),
{{'^', Meta, [Var]}, EA};
false ->
form_error(Meta, ?key(EA, file), ?MODULE, {unbound_variable_pin, VarName})
@@ -332,27 +332,43 @@ expand({'_', _Meta, Kind} = Var, #{context := match} = E) when is_atom(Kind) ->
expand({'_', Meta, Kind}, E) when is_atom(Kind) ->
form_error(Meta, ?key(E, file), ?MODULE, unbound_underscore);
expand({Name, Meta, Kind} = Var, #{context := match, export_vars := Export} = E) when is_atom(Name), is_atom(Kind) ->
Pair = {Name, var_kind(Meta, Kind)},
NewVars = ordsets:add_element(Pair, ?key(E, vars)),
NewExport = case (Export /= nil) of
true -> ordsets:add_element(Pair, Export);
false -> Export
end,
{Var, E#{vars := NewVars, export_vars := NewExport}};
expand({Name, Meta, Kind} = Var, #{context := match} = E) when is_atom(Name), is_atom(Kind) ->
#{vars := Vars, match_vars := Match, export_vars := Export} = E,
Pair = {Name, var_context(Meta, Kind)},
NewVars = ordsets:add_element(Pair, Vars),
NewExport =
case (Export /= nil) of
true -> ordsets:add_element(Pair, Export);
false -> Export
end,
NewMatch =
case lists:member(Pair, Match) of
true -> warn_underscored_var_repeat(Meta, Name, Kind, E), Match;
false -> [Pair | Match]
end,
{Var, E#{vars := NewVars, match_vars := NewMatch, export_vars := NewExport}};
expand({Name, Meta, Kind} = Var, #{vars := Vars} = E) when is_atom(Name), is_atom(Kind) ->
case lists:member({Name, var_kind(Meta, Kind)}, Vars) of
case lists:member({Name, var_context(Meta, Kind)}, Vars) of
true ->
warn_underscored_var_access(Meta, Name, Kind, E),
{Var, E};
false ->
case lists:keyfind(var, 1, Meta) of
{var, true} ->
form_error(Meta, ?key(E, file), ?MODULE, {undefined_var, Name, Kind});
_ ->
Message =
io_lib:format("variable \"~ts\" does not exist and is being expanded to \"~ts()\","
" please use parentheses to remove the ambiguity or change the variable name", [Name, Name]),
elixir_errors:warn(?line(Meta), ?key(E, file), Message),
case ?key(E, match_vars) of
warn ->
Message =
io_lib:format("variable \"~ts\" does not exist and is being expanded to \"~ts()\","
" please use parentheses to remove the ambiguity or change the variable name", [Name, Name]),
elixir_errors:warn(?line(Meta), ?key(E, file), Message);
apply ->
ok
end,
expand({Name, Meta, []}, E)
end
end;
@@ -391,6 +407,7 @@ expand({{'.', DotMeta, [Left, Right]}, Meta, Args}, E)
%% Anonymous calls
expand({{'.', DotMeta, [Expr]}, Meta, Args}, E) when is_list(Args) ->
assert_no_match_or_guard_scope(Meta, "anonymous call", E),
{EExpr, EE} = expand(Expr, E),
if
is_atom(EExpr) ->
@@ -434,11 +451,12 @@ expand(Function, E) when is_function(Function) ->
expand(PidOrRef, E) when is_pid(PidOrRef); is_reference(PidOrRef) ->
case ?key(E, function) of
nil ->
PidOrRef;
{PidOrRef, E};
Function ->
%% TODO: Make me an error on 2.0
elixir_errors:form_warn([], ?key(E, file), ?MODULE,
{invalid_pid_or_ref_in_function, PidOrRef, Function})
{invalid_pid_or_ref_in_function, PidOrRef, Function}),
{PidOrRef, E}
end;
expand(Other, E) when is_number(Other); is_atom(Other); is_binary(Other) ->
@@ -554,7 +572,7 @@ expand_args(Args, E) ->
%% Match/var helpers
var_kind(Meta, Kind) ->
var_context(Meta, Kind) ->
case lists:keyfind(counter, 1, Meta) of
{counter, Counter} -> Counter;
false -> Kind
@@ -563,7 +581,6 @@ var_kind(Meta, Kind) ->
%% Case
expand_case(true, Meta, Expr, Opts, E) ->
assert_no_match_or_guard_scope(Meta, 'case', E),
{EExpr, EE} = expand(Expr, E),
{EOpts, EO} = elixir_clauses:'case'(Meta, Opts, EE),
ROpts =
@@ -789,11 +806,11 @@ assert_function_scope(_Meta, _Kind, #{function := Function}) -> Function.
assert_no_match_or_guard_scope(Meta, Kind, E) ->
assert_no_match_scope(Meta, Kind, E),
assert_no_guard_scope(Meta, Kind, E).
assert_no_match_scope(Meta, _Kind, #{context := match, file := File}) ->
form_error(Meta, File, ?MODULE, invalid_pattern_in_match);
assert_no_match_scope(Meta, Kind, #{context := match, file := File}) ->
form_error(Meta, File, ?MODULE, {invalid_pattern_in_match, Kind});
assert_no_match_scope(_Meta, _Kind, _E) -> [].
assert_no_guard_scope(Meta, _Kind, #{context := guard, file := File}) ->
form_error(Meta, File, ?MODULE, invalid_expr_in_guard);
assert_no_guard_scope(Meta, Kind, #{context := guard, file := File}) ->
form_error(Meta, File, ?MODULE, {invalid_expr_in_guard, Kind});
assert_no_guard_scope(_Meta, _Kind, _E) -> [].
%% Here we look into the Clauses "optimistically", that is, we don't check for
@@ -814,6 +831,30 @@ assert_no_underscore_clause_in_cond(_Other, _E) ->
%% Warnings
warn_underscored_var_repeat(Meta, Name, Kind, E) ->
Warn = should_warn(Meta),
case atom_to_list(Name) of
"_" ++ _ when Warn ->
elixir_errors:form_warn(Meta, ?key(E, file), ?MODULE, {underscored_var_repeat, Name, Kind});
_ ->
ok
end.
warn_underscored_var_access(Meta, Name, Kind, E) ->
Warn = should_warn(Meta),
case atom_to_list(Name) of
"_" ++ _ when Warn ->
elixir_errors:form_warn(Meta, ?key(E, file), ?MODULE, {underscored_var_access, Name, Kind});
_ ->
ok
end.
context_info(Kind) when Kind == nil; is_integer(Kind) -> "";
context_info(Kind) -> io_lib:format(" (context ~ts)", [elixir_aliases:inspect(Kind)]).
should_warn(Meta) ->
lists:keyfind(generated, 1, Meta) /= {generated, true}.
format_error({useless_literal, Term}) ->
io_lib:format("code block contains unused literal ~ts "
"(remove the literal or assign it to _ to avoid warnings)",
@@ -866,14 +907,14 @@ format_error({undefined_var, Name, Kind}) ->
Message =
"expected variable \"~ts\"~ts to expand to an existing variable "
"or be part of a match",
io_lib:format(Message, [Name, elixir_erl_var:context_info(Kind)]);
io_lib:format(Message, [Name, context_info(Kind)]);
format_error(underscore_in_cond) ->
"unbound variable _ inside \"cond\". If you want the last clause to always match, "
"you probably meant to use: true ->";
format_error(invalid_expr_in_guard) ->
"invalid expression in guard";
format_error(invalid_pattern_in_match) ->
"invalid pattern in match";
format_error({invalid_expr_in_guard, Kind}) ->
io_lib:format("invalid expression in guard, ~ts is not allowed in guards", [Kind]);
format_error({invalid_pattern_in_match, Kind}) ->
io_lib:format("invalid pattern in match, ~ts is not allowed in matches", [Kind]);
format_error({invalid_expr_in_scope, Scope, Kind}) ->
io_lib:format("cannot invoke ~ts outside ~ts", [Kind, Scope]);
format_error({invalid_alias, Expr}) ->
@@ -918,4 +959,15 @@ format_error({options_are_not_keyword, Kind, Opts}) ->
io_lib:format("invalid options for ~s, expected a keyword list, got: ~ts",
[Kind, 'Elixir.Macro':to_string(Opts)]);
format_error({undefined_function, Name, Args}) ->
io_lib:format("undefined function ~ts/~B", [Name, length(Args)]).
io_lib:format("undefined function ~ts/~B", [Name, length(Args)]);
format_error({underscored_var_repeat, Name, Kind}) ->
io_lib:format("the underscored variable \"~ts\"~ts appears more than once in a "
"match. This means the pattern will only match if all \"~ts\" bind "
"to the same value. If this is the intended behaviour, please "
"remove the leading underscore from the variable name, otherwise "
"give the variables different names", [Name, context_info(Kind), Name]);
format_error({underscored_var_access, Name, Kind}) ->
io_lib:format("the underscored variable \"~ts\"~ts is used after being set. "
"A leading underscore indicates that the value of the variable "
"should be ignored. If this is intended please rename the "
"variable to remove the underscore", [Name, context_info(Kind)]).
+24 -12
View File
@@ -1,7 +1,7 @@
%% Module responsible for tracking invocations of module calls.
-module(elixir_locals).
-export([
setup/1, cleanup/1, cache_env/1, get_cached_env/1,
setup/1, cleanup/1, cache_env/1, cache_env/2, get_cached_env/1,
record_local/2, record_local/3, record_import/4,
record_definition/3, record_defaults/4,
ensure_no_import_conflict/3, warn_unused_local/3, format_error/1
@@ -9,6 +9,7 @@
-include("elixir.hrl").
-define(attr, {elixir, locals_tracker}).
-define(cache, {elixir, cache_env}).
-define(tracker, 'Elixir.Module.LocalsTracker').
setup(Module) ->
@@ -56,18 +57,29 @@ if_tracker(Module, Default, Callback) ->
%% CACHING
cache_env(#{module := Module, line := Line} = E) ->
try ets:lookup_element(elixir_module:data_table(Module), ?attr, 2) of
Pid ->
{Pid, {Line, ?tracker:cache_env(Pid, E#{line := nil, vars := []})}}
catch
error:badarg ->
{Escaped, _} = elixir_quote:escape(E#{vars := []}, false),
Escaped
end.
cache_env(#{module := Module} = E) ->
cache_env(elixir_module:data_table(Module), E).
get_cached_env({Pid, {Line, Ref}}) -> (?tracker:get_cached_env(Pid, Ref))#{line := Line};
get_cached_env(Env) -> Env.
cache_env(Table, #{line := Line} = E) ->
Cache = E#{line := nil, vars := []},
Pos =
case ets:lookup(Table, ?cache) of
[{_, Key, Cache}] ->
Key;
[{_, PrevKey, _}] ->
Key = PrevKey + 1,
ets:insert(Table, {{cache_env, Key}, Cache}),
ets:insert(Table, {?cache, Key, Cache}),
Key
end,
{Table, {Line, Pos}}.
get_cached_env({Table, {Line, Pos}}) ->
(ets:lookup_element(Table, {cache_env, Pos}, 2))#{line := Line};
get_cached_env(Env) ->
Env.
%% ERROR HANDLING
+2 -1
View File
@@ -210,7 +210,8 @@ build(Line, File, Module, Lexical) ->
{typep, [], true, nil},
% Internal
{{elixir, impls}, []}
{{elixir, impls}, []},
{{elixir, cache_env}, 0, nil}
]),
Persisted = [behaviour, on_load, compile, external_resource, dialyzer, vsn],
+6 -10
View File
@@ -15,15 +15,15 @@ overridable(Module, Value) ->
super(Meta, File, Module, Function) ->
case store(Module, Function, true) of
{ok, Name} ->
Name;
{_, _} = KindName ->
KindName;
error ->
elixir_errors:form_error(Meta, File, ?MODULE, {no_super, Module, Function})
end.
store_pending(Module) ->
[begin
{ok, _} = store(Module, Pair, false),
{_, _} = store(Module, Pair, false),
Pair
end || {Pair, {_, _, _, false}} <- maps:to_list(overridable(Module)),
not 'Elixir.Module':'defines?'(Module, Pair)].
@@ -42,7 +42,7 @@ store(Module, Function, Hidden) ->
false ->
{Kind, Name, Arity, Clauses};
true when Kind == defmacro; Kind == defmacrop ->
{defp, name(Name, Count), Arity + 1, rewrite_clauses(Clauses)};
{defmacrop, name(Name, Count), Arity, Clauses};
true ->
{defp, name(Name, Count), Arity, Clauses}
end,
@@ -62,15 +62,11 @@ store(Module, Function, Hidden) ->
ok
end,
{ok, Tuple};
{FinalKind, FinalName};
error ->
error
end.
rewrite_clauses(Clauses) ->
[{Meta, [{'__CALLER__', [], nil} | Args], Guards, Body} ||
{Meta, Args, Guards, Body} <- Clauses].
name(Name, Count) when is_integer(Count) ->
list_to_atom(atom_to_list(Name) ++ " (overridable " ++ integer_to_list(Count) ++ ")").
@@ -83,6 +79,6 @@ format_error({no_super, Module, {Name, Arity}}) ->
[Name, Arity, elixir_aliases:inspect(Module), Joined]).
format_fa({Name, Arity}) ->
A = atom_to_binary(Name, utf8),
A = 'Elixir.Inspect.Function':escape_name(Name),
B = integer_to_binary(Arity),
<<A/binary, $/, B/binary>>.
-105
View File
@@ -1,105 +0,0 @@
-module(elixir_with).
-export([expand/3]).
-include("elixir.hrl").
expand(Meta, Args, Env) ->
{Exprs, Opts} =
case elixir_utils:split_last(Args) of
{_, LastArg} = SplitResult when is_list(LastArg) ->
SplitResult;
_ ->
{Args, []}
end,
{DoExpr, OtherOpts1} =
case lists:keytake(do, 1, Opts) of
{value, {do, DoValue}, RestOpts1} ->
{DoValue, RestOpts1};
false ->
elixir_errors:form_error(Meta, ?key(Env, file), elixir_expand, {missing_option, 'with', [do]})
end,
{ElseExpr, OtherOpts2} =
case lists:keytake(else, 1, OtherOpts1) of
{value, {else, ElseValue}, RestOpts2} ->
assert_clauses(Meta, ElseValue, Env),
{ElseValue, RestOpts2};
false ->
{nil, OtherOpts1}
end,
case OtherOpts2 of
[{Key, _} | _] ->
elixir_errors:form_error(Meta, ?key(Env, file), elixir_clauses, {unexpected_option, with, Key});
[] ->
ok
end,
ResultCase =
case ElseExpr of
nil ->
{MainCase, _} = build_main_case(Exprs, DoExpr, fun(Ret) -> Ret end, false),
MainCase;
_ ->
Wrapper = fun(Ret) -> {error, Ret} end,
case build_main_case(Exprs, {ok, DoExpr}, Wrapper, false) of
{MainCase, false} ->
Message =
"\"else\" clauses will never match"
" because all patterns in \"with\" will always match",
elixir_errors:warn(?line(Meta), ?key(Env, file), Message),
{{'.', Meta, [erlang, element]}, Meta, [MainCase, 2]};
{MainCase, true} ->
build_else_case(Meta, MainCase, ElseExpr, Wrapper)
end
end,
elixir_expand:expand(ResultCase, Env).
%% Helpers
assert_clauses(_Meta, [], _Env) ->
ok;
assert_clauses(Meta, [{'->', _, [_, _]} | Rest], Env) ->
assert_clauses(Meta, Rest, Env);
assert_clauses(Meta, _Other, Env) ->
elixir_errors:form_error(Meta, ?key(Env, file), elixir_clauses, {bad_or_missing_clauses, {with, else}}).
build_main_case([{'<-', Meta, [{Name, _, Ctx}, _] = Args} | Rest], DoExpr, Wrapper, HasMatch)
when is_atom(Name) andalso is_atom(Ctx) ->
build_main_case([{'=', Meta, Args} | Rest], DoExpr, Wrapper, HasMatch);
build_main_case([{'<-', Meta, [Left, Right]} | Rest], DoExpr, Wrapper, _HasMatch) ->
{InnerCase, true} = build_main_case(Rest, DoExpr, Wrapper, true),
Generated = ?generated(Meta),
Other = {other, Generated, ?var_context},
Clauses = [
{'->', Generated, [[Left], InnerCase]},
{'->', Generated, [[Other], Wrapper(Other)]}
],
{{'case', [{export_vars, false} | Meta], [Right, [{do, Clauses}]]}, true};
build_main_case([Expr | Rest], DoExpr, Wrapper, HasMatch) ->
{InnerCase, InnerHasMatch} = build_main_case(Rest, DoExpr, Wrapper, HasMatch),
{{'__block__', [], [Expr, InnerCase]}, InnerHasMatch};
build_main_case([], DoExpr, _Wrapper, HasMatch) ->
{DoExpr, HasMatch}.
build_else_case(Meta, MainCase, Clauses, Wrapper) ->
Generated = ?generated(Meta),
Return = {return, Generated, ?var_context},
ReturnClause = {'->', Generated, [[{ok, Return}], Return]},
Other = {other, Generated, ?var_context},
RaiseError = {{'.', Generated, [erlang, error]}, Meta, [{with_clause, Other}]},
RaiseErrorClause = {'->', Generated, [[Wrapper(Other)], RaiseError]},
ClauseWrapper = fun(Clause) -> wrap_clause_pattern(Clause, Wrapper) end,
ResultClauses = [ReturnClause] ++ lists:map(ClauseWrapper, Clauses) ++ [RaiseErrorClause],
{'case', [{export_vars, false} | Meta], [MainCase, [{do, ResultClauses}]]}.
wrap_clause_pattern({'->', Meta, [[Left], Right]}, Wrapper) ->
{'->', Meta, [[wrap_pattern(Left, Wrapper)], Right]}.
wrap_pattern({'when', Meta, [Left, Right]}, Wrapper) ->
{'when', Meta, [Wrapper(Left), Right]};
wrap_pattern(Expr, Wrapper) ->
Wrapper(Expr).
+26
View File
@@ -750,4 +750,30 @@ defmodule BaseTest do
test "hex_decode32!/2 with :mixed case and ignoring padding" do
assert "fo" == hex_decode32!("cPNg", case: :mixed, padding: false)
end
test "encode then decode is identity" do
for {encode, decode} <- [{&encode16/2, &decode16!/2},
{&encode32/2, &decode32!/2},
{&hex_encode32/2, &hex_decode32!/2},
{&encode64/2, &decode64!/2},
{&url_encode64/2, &url_decode64!/2}],
encode_case <- [:upper, :lower],
decode_case <- [:upper, :lower, :mixed],
(encode_case == decode_case) or (decode_case == :mixed),
pad? <- [true, false],
len <- 0..256 do
data =
0
|> :lists.seq(len - 1)
|> Enum.shuffle()
|> IO.iodata_to_binary()
expected =
data
|> encode.([case: encode_case, pad: pad?])
|> decode.([case: decode_case, pad: pad?])
assert data == expected, "identity did not match for #{inspect data} when #{inspect encode} (#{encode_case})"
end
end
end
@@ -1,5 +1,5 @@
Code.require_file "../test_helper.exs", __DIR__
Code.require_file "../fixtures/calendar/julian.exs", __DIR__
Code.require_file "../fixtures/calendar/holocene.exs", __DIR__
defmodule Date.RangeTest do
use ExUnit.Case, async: true
@@ -13,7 +13,7 @@ defmodule Date.RangeTest do
assert Enum.member?(@asc_range, ~D[2000-01-01])
assert Enum.member?(@asc_range, ~D[2001-01-01])
refute Enum.member?(@asc_range, ~D[2002-01-01])
refute Enum.member?(@asc_range, Calendar.Julian.date(1999, 12, 19))
refute Enum.member?(@asc_range, Calendar.Holocene.date(12000, 1, 1))
end
test "for descending range" do
@@ -21,7 +21,7 @@ defmodule Date.RangeTest do
assert Enum.member?(@desc_range, ~D[2000-01-01])
assert Enum.member?(@desc_range, ~D[2001-01-01])
refute Enum.member?(@desc_range, ~D[1999-01-01])
refute Enum.member?(@asc_range, Calendar.Julian.date(1999, 12, 19))
refute Enum.member?(@asc_range, Calendar.Holocene.date(12000, 1, 1))
end
end
@@ -49,16 +49,16 @@ defmodule Date.RangeTest do
test "both dates must have matching calendars" do
first = ~D[2000-01-01]
last = Calendar.Julian.date(2001, 01, 01)
last = Calendar.Holocene.date(12001, 1, 1)
assert_raise ArgumentError, "both dates must have matching calendars", fn ->
Date.range(first, last)
end
end
test "accepts equal but not Calendar.ISO calendars" do
first = Calendar.Julian.date(2000, 01, 01)
last = Calendar.Julian.date(2001, 01, 01)
test "accepts equal but non Calendar.ISO calendars" do
first = Calendar.Holocene.date(12000, 1, 1)
last = Calendar.Holocene.date(12001, 1, 1)
range = Date.range(first, last)
assert range
assert first in range
+53 -18
View File
@@ -1,5 +1,5 @@
Code.require_file "test_helper.exs", __DIR__
Code.require_file "fixtures/calendar/julian.exs", __DIR__
Code.require_file "fixtures/calendar/holocene.exs", __DIR__
defmodule FakeCalendar do
def date_to_string(_, _, _), do: "boom"
@@ -37,7 +37,10 @@ defmodule DateTest do
test "compare/2 across calendars" do
date1 = ~D[2000-01-01]
date2 = Calendar.Julian.date(2000, 01, 01)
date2 = Calendar.Holocene.date(12000, 01, 01)
assert Date.compare(date1, date2) == :eq
date2 = Calendar.Holocene.date(12001, 01, 01)
assert Date.compare(date1, date2) == :lt
assert Date.compare(date2, date1) == :gt
end
@@ -53,14 +56,14 @@ defmodule DateTest do
end
test "convert/2" do
assert Date.convert(~D[2000-01-01], Calendar.Julian) ==
{:ok, Calendar.Julian.date(1999, 12, 19)}
assert ~D[2000-01-01] |> Date.convert!(Calendar.Julian) |> Date.convert!(Calendar.ISO) ==
assert Date.convert(~D[2000-01-01], Calendar.Holocene) ==
{:ok, Calendar.Holocene.date(12000, 01, 01)}
assert ~D[2000-01-01] |> Date.convert!(Calendar.Holocene) |> Date.convert!(Calendar.ISO) ==
~D[2000-01-01]
assert Date.convert(~D[2016-02-03], FakeCalendar) ==
{:error, :incompatible_calendars}
assert Date.convert(~N[2000-01-01 00:00:00], Calendar.Julian) ==
{:ok, Calendar.Julian.date(1999, 12, 19)}
assert Date.convert(~N[2000-01-01 00:00:00], Calendar.Holocene) ==
{:ok, Calendar.Holocene.date(12000, 01, 01)}
end
test "diff/2" do
@@ -68,7 +71,7 @@ defmodule DateTest do
assert Date.diff(~D[2000-01-01], ~D[2000-01-31]) == -30
date1 = ~D[2000-01-01]
date2 = Calendar.Julian.date(2000, 01, 01)
date2 = Calendar.Holocene.date(12000, 01, 14)
assert Date.diff(date1, date2) == -13
assert Date.diff(date2, date1) == 13
end
@@ -145,33 +148,40 @@ defmodule NaiveDateTimeTest do
test "add/2 with other calendars" do
assert ~N[2000-01-01 12:34:15.123456]
|> NaiveDateTime.convert!(Calendar.Julian)
|> NaiveDateTime.convert!(Calendar.Holocene)
|> NaiveDateTime.add(10, :second) ==
%NaiveDateTime{calendar: Calendar.Julian, year: 1999, month: 12, day: 19,
%NaiveDateTime{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1,
hour: 12, minute: 34, second: 25, microsecond: {123456, 6}}
end
test "diff/2 with other calendars" do
assert ~N[2000-01-01 12:34:15.123456]
|> NaiveDateTime.convert!(Calendar.Julian)
|> NaiveDateTime.convert!(Calendar.Holocene)
|> NaiveDateTime.add(10, :second)
|> NaiveDateTime.diff(~N[2000-01-01 12:34:15.123456]) == 10
end
test "convert/2" do
assert NaiveDateTime.convert(~N[2000-01-01 12:34:15.123400], Calendar.Julian) ==
{:ok, Calendar.Julian.naive_datetime(1999, 12, 19, 12, 34, 15, 123400)}
assert NaiveDateTime.convert(~N[2000-01-01 12:34:15.123400], Calendar.Holocene) ==
{:ok, Calendar.Holocene.naive_datetime(12000, 1, 1, 12, 34, 15, {123400, 6})}
assert ~N[2000-01-01 12:34:15]
|> NaiveDateTime.convert!(Calendar.Holocene)
|> NaiveDateTime.convert!(Calendar.ISO) ==
~N[2000-01-01 12:34:15]
assert ~N[2000-01-01 12:34:15.123456]
|> NaiveDateTime.convert!(Calendar.Julian)
|> NaiveDateTime.convert!(Calendar.Holocene)
|> NaiveDateTime.convert!(Calendar.ISO) ==
~N[2000-01-01 12:34:15.123456]
assert NaiveDateTime.convert(~N[2016-02-03 00:00:01], FakeCalendar) ==
{:error, :incompatible_calendars}
assert NaiveDateTime.convert(~N[1970-01-01 00:00:00], Calendar.Julian) ==
{:ok, Calendar.Julian.naive_datetime(1969, 12, 19, 0, 0, 0, {0, 0})}
assert NaiveDateTime.convert(DateTime.from_unix!(0, :seconds), Calendar.Julian) ==
{:ok, Calendar.Julian.naive_datetime(1969, 12, 19, 0, 0, 0, {0, 0})}
assert NaiveDateTime.convert(~N[1970-01-01 00:00:00], Calendar.Holocene) ==
{:ok, Calendar.Holocene.naive_datetime(11970, 1, 1, 0, 0, 0, {0, 0})}
assert NaiveDateTime.convert(DateTime.from_unix!(0, :seconds), Calendar.Holocene) ==
{:ok, Calendar.Holocene.naive_datetime(11970, 1, 1, 0, 0, 0, {0, 0})}
end
end
@@ -260,6 +270,31 @@ defmodule DateTimeTest do
assert DateTime.compare(datetime2, datetime1) == :gt
end
test "convert/2" do
datetime_iso = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
hour: 23, minute: 0, second: 7, microsecond: {0, 0},
utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
datetime_hol = %DateTime{year: 12000, month: 2, day: 29, zone_abbr: "CET",
hour: 23, minute: 0, second: 7, microsecond: {0, 0},
utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw",
calendar: Calendar.Holocene}
assert DateTime.convert(datetime_iso, Calendar.Holocene) == {:ok, datetime_hol}
assert datetime_iso
|> DateTime.convert!(Calendar.Holocene)
|> DateTime.convert!(Calendar.ISO) ==
datetime_iso
assert %{datetime_iso | microsecond: {123, 6}}
|> DateTime.convert!(Calendar.Holocene)
|> DateTime.convert!(Calendar.ISO) ==
%{datetime_iso | microsecond: {123, 6}}
assert DateTime.convert(datetime_iso, FakeCalendar) ==
{:error, :incompatible_calendars}
end
test "from_iso8601/1 with tz offsets" do
assert DateTime.from_iso8601("2017-06-02T14:00:00+01:00") |> elem(1) ==
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
+51 -9
View File
@@ -59,6 +59,19 @@ defmodule EnumTest do
assert Enum.chunk([1, 2, 3, 4, 5], 4, 4, 6..10) == [[1, 2, 3, 4], [5, 6, 7, 8]]
end
test "chunk_every/2" do
assert Enum.chunk_every([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]]
end
test "chunk_every/4" do
assert Enum.chunk_every([1, 2, 3, 4, 5], 2, 2, [6]) == [[1, 2], [3, 4], [5, 6]]
assert Enum.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, :discard) == [[1, 2, 3], [3, 4, 5]]
assert Enum.chunk_every([1, 2, 3, 4, 5, 6], 2, 3, :discard) == [[1, 2], [4, 5]]
assert Enum.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, []) == [[1, 2, 3], [3, 4, 5], [5, 6]]
assert Enum.chunk_every([1, 2, 3, 4, 5, 6], 3, 3, []) == [[1, 2, 3], [4, 5, 6]]
assert Enum.chunk_every([1, 2, 3, 4, 5], 4, 4, 6..10) == [[1, 2, 3, 4], [5, 6, 7, 8]]
end
test "chunk_by/2" do
assert Enum.chunk_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1)) == [[1], [2, 2], [3], [4, 4, 6], [7, 7]]
assert Enum.chunk_by([1, 2, 3, 4], fn _ -> true end) == [[1, 2, 3, 4]]
@@ -66,12 +79,15 @@ defmodule EnumTest do
assert Enum.chunk_by([1], fn _ -> true end) == [[1]]
end
test "chunk_by/4" do
test "chunk_while/4" do
chunk_fun = fn i, acc ->
if rem(i, 2) == 0 do
{:cont, Enum.reverse([i | acc]), []}
else
{:cont, [i | acc]}
cond do
i > 10 ->
{:halt, acc}
rem(i, 2) == 0 ->
{:cont, Enum.reverse([i | acc]), []}
true ->
{:cont, [i | acc]}
end
end
@@ -80,12 +96,16 @@ defmodule EnumTest do
acc -> {:cont, Enum.reverse(acc), []}
end
assert Enum.chunk_by([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [], chunk_fun, after_fun) ==
assert Enum.chunk_while([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [], chunk_fun, after_fun) ==
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert Enum.chunk_by(0..10, [], chunk_fun, after_fun) ==
assert Enum.chunk_while(0..9, [], chunk_fun, after_fun) ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9]]
assert Enum.chunk_while(0..10, [], chunk_fun, after_fun) ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert Enum.chunk_by(0..11, [], chunk_fun, after_fun) ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]
assert Enum.chunk_while(0..11, [], chunk_fun, after_fun) ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert Enum.chunk_while([5, 7, 9, 11], [], chunk_fun, after_fun) ==
[[5, 7, 9]]
end
test "concat/1" do
@@ -833,6 +853,18 @@ defmodule EnumTest.Range do
assert Enum.chunk(1..5, 4, 4, 6..10) == [[1, 2, 3, 4], [5, 6, 7, 8]]
end
test "chunk_every/2" do
assert Enum.chunk_every(1..5, 2) == [[1, 2], [3, 4], [5]]
end
test "chunk_every/4" do
assert Enum.chunk_every(1..5, 2, 2) == [[1, 2], [3, 4], [5]]
assert Enum.chunk_every(1..6, 3, 2, :discard) == [[1, 2, 3], [3, 4, 5]]
assert Enum.chunk_every(1..6, 2, 3, :discard) == [[1, 2], [4, 5]]
assert Enum.chunk_every(1..6, 3, 2, []) == [[1, 2, 3], [3, 4, 5], [5, 6]]
assert Enum.chunk_every(1..5, 4, 4, 6..10) == [[1, 2, 3, 4], [5, 6, 7, 8]]
end
test "chunk_by/2" do
assert Enum.chunk_by(1..4, fn _ -> true end) == [[1, 2, 3, 4]]
assert Enum.chunk_by(1..4, &(rem(&1, 2) == 1)) == [[1], [2], [3], [4]]
@@ -1566,3 +1598,13 @@ defmodule EnumTest.SideEffects do
assert Enum.take(iterator, 0) == []
end
end
defmodule EnumTest.Function do
use ExUnit.Case, async: true
test "raises Protocol.UndefinedError for funs of wrong arity" do
assert_raise Protocol.UndefinedError, fn ->
Enum.to_list(fn -> nil end)
end
end
end
+9
View File
@@ -460,6 +460,11 @@ defmodule FileTest do
end
end
test "cp_r raises on path with null byte" do
assert_raise ArgumentError, ~r/null byte/, fn -> File.cp_r("source", "foo\0bar") end
assert_raise ArgumentError, ~r/null byte/, fn -> File.cp_r("foo\0bar", "dest") end
end
test "cp_r with src file and dest file" do
src = fixture_path("file.txt")
dest = tmp_path("sample.txt")
@@ -1113,6 +1118,10 @@ defmodule FileTest do
refute File.exists?(fixture)
end
test "rm_rf raises on path with null byte" do
assert_raise ArgumentError, ~r/null byte/, fn -> File.rm_rf("foo\0bar") end
end
test "rm_rf with symlink" do
from = tmp_path("tmp/from")
to = tmp_path("tmp/to")
@@ -0,0 +1,44 @@
defmodule Calendar.Holocene do
# This calendar is used to test conversions between calendars.
# It implements the Holocene calendar, which is based on the
# Propleptic Gregorian calendar with every year + 10000.
def date(year, month, day) do
%Date{year: year, month: month, day: day, calendar: __MODULE__}
end
def naive_datetime(year, month, day, hour, minute, second, microsecond \\ {0, 0}) do
%NaiveDateTime{year: year, month: month, day: day, hour: hour, minute: minute, second: second,
microsecond: microsecond, calendar: __MODULE__}
end
def date_to_string(year, month, day) do
"#{year}-#{month}-#{day} (HE)"
end
def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
"#{year}-#{month}-#{day} #{Calendar.ISO.time_to_string(hour, minute, second, microsecond)} (HE)"
end
defdelegate time_to_string(hour, minute, second, microsecond), to: Calendar.ISO
def day_rollover_relative_to_midnight_utc(), do: {0, 1}
def naive_datetime_from_iso_days(entry) do
{year, month, day, hour, minute, second, microsecond} =
Calendar.ISO.naive_datetime_from_iso_days(entry)
{year + 10000, month, day, hour, minute, second, microsecond}
end
def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
Calendar.ISO.naive_datetime_to_iso_days(year - 10000, month, day, hour, minute, second, microsecond)
end
defdelegate time_from_day_fraction(day_fraction), to: Calendar.ISO
defdelegate time_to_day_fraction(hour, minute, second, microsecond), to: Calendar.ISO
defdelegate leap_year?(year), to: Calendar.ISO
defdelegate days_in_month(year, month), to: Calendar.ISO
end
@@ -1,99 +0,0 @@
defmodule Calendar.Julian do
# This calendar is used to test conversions between calendars.
# It implements the Julian calendar.
import Integer, only: [floor_div: 2]
def date_to_string(year, month, day) do
"#{year}-#{month}-#{day} (O.S.)"
end
def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
"#{year}-#{month}-#{day} #{Calendar.ISO.time_to_string(hour, minute, second, microsecond)} (O.S.)"
end
def time_to_string(hour, minute, second, microsecond) do
Calendar.ISO.time_to_string(hour, minute, second, microsecond)
end
def date(year, month, day) do
%Date{year: year, month: month, day: day, calendar: Calendar.Julian}
end
def naive_datetime(year, month, day, hour, minute, second, microsecond \\ {0, 0})
def naive_datetime(year, month, day, hour, minute, second, microsecond) when is_integer(microsecond) do
naive_datetime(year, month, day, hour, minute, second, {microsecond, 6})
end
def naive_datetime(year, month, day, hour, minute, second, microsecond) do
%NaiveDateTime{year: year, month: month, day: day, hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: Calendar.Julian}
end
def day_rollover_relative_to_midnight_utc(), do: {0, 1}
def naive_datetime_from_rata_die({days, day_fraction}) do
{year, month, day} = date_from_rata_die(days)
{hour, minute, second, microsecond} = time_from_day_fraction(day_fraction)
{year, month, day, hour, minute, second, microsecond}
end
defp date_from_rata_die(days) do
approx = floor_div((4 * (days - epoch())) + 1464, 1461)
year = if approx <= 0, do: approx - 1, else: approx
prior_days = days - date_to_rata_die(year, 1, 1)
correction = cond do
days < date_to_rata_die(year, 3, 1) -> 0
leap_year?(year) -> 1
true -> 2
end
month = floor_div(12*(prior_days + correction) + 373, 367)
day = 1 + days - date_to_rata_die(year, month, 1)
{year, month, day}
end
def naive_datetime_to_rata_die(year, month, day, hour, minute, second, microsecond) do
days = date_to_rata_die(year, month, day)
day_fraction = time_to_day_fraction(hour, minute, second, microsecond)
{days, day_fraction}
end
def time_from_day_fraction(day_fraction), do: Calendar.ISO.time_from_day_fraction(day_fraction)
def time_to_day_fraction(hour, minute, second, microsecond) when is_integer(microsecond) do
time_to_day_fraction(hour, minute, second, {microsecond, 6})
end
def time_to_day_fraction(hour, minute, second, microsecond) do
Calendar.ISO.time_to_day_fraction(hour, minute, second, microsecond)
end
def leap_year?(year) when is_integer(year) and year >= 0 do
rem(year, 4) == if year > 0, do: 0, else: 3
end
def days_in_month(year, month)
def days_in_month(year, 2) do
if leap_year?(year), do: 29, else: 28
end
def days_in_month(_, month) when month in [4, 6, 9, 11], do: 30
def days_in_month(_, month) when month in 1..12, do: 31
defp date_to_rata_die(year, month, day) do
year = if year < 0, do: year + 1, else: year
epoch() - 1 + (365 * (year - 1)) + floor_div(year - 1, 4) +
floor_div(367 * month - 362, 12) +
adjustment_for_leap_year(year, month) + day
end
defp epoch(), do: -1
defp adjustment_for_leap_year(year, month) do
cond do
month <= 2 -> 0
leap_year?(year) -> -1
true -> -2
end
end
end
+8
View File
@@ -452,6 +452,14 @@ defmodule Inspect.MapTest do
inspect(%Inspect.Error{message: "#{msg}"})
end
test "bad implementation safe disables colors" do
msg = "got KeyError with message \\\"key :unknown not found in: " <>
"%{__struct__: Inspect.MapTest.Failing, key: 0}\\\" while " <>
"inspecting %{__struct__: Inspect.MapTest.Failing, key: 0}"
assert inspect(%Failing{}, syntax_colors: [atom: [:green]]) =~ msg
end
test "exception" do
assert inspect(%RuntimeError{message: "runtime error"}) ==
"%RuntimeError{message: \"runtime error\"}"
@@ -15,6 +15,12 @@ defmodule Kernel.ErrorsTest do
'[foo: \u200B]\noops'
end
test "invalid __CALLER__" do
assert_compile_fail CompileError,
"nofile:1: variable '__CALLER__' is unbound",
'defmodule Sample do def hello do __CALLER__ end end'
end
test "invalid quoted token" do
assert_compile_fail SyntaxError,
"nofile:1: syntax error before: \"world\"",
@@ -169,10 +169,6 @@ defmodule Kernel.ExpansionTest do
assert expand(quote do: __DIR__) == __DIR__
end
test "__CALLER__" do
assert expand(quote do: __CALLER__) == quote do: __CALLER__
end
test "__ENV__" do
env = %{__ENV__ | line: 0}
assert expand_env(quote(do: __ENV__), env) ==
@@ -498,50 +494,18 @@ defmodule Kernel.ExpansionTest do
describe "with" do
test "variables do not leak" do
input = quote(do: (with({foo} <- {bar}, do: baz = :ok); baz))
other = Macro.var(:other, :elixir_with)
result = quote do
case {bar()} do
{foo} -> baz = :ok
unquote(other) -> unquote(other)
end
baz()
end
assert input |> expand() |> clean_meta([:export_vars, :generated]) == result
assert expand(quote(do: (with({foo} <- {bar}, do: baz = :ok); baz)))
quote(do: (with({foo} <- {bar()}, do: baz = :ok); baz()))
end
test "variables are available in do option" do
input = quote(do: (with({foo} <- {bar}, do: baz = foo); baz))
other = Macro.var(:other, :elixir_with)
result = quote do
case {bar()} do
{foo} -> baz = foo
unquote(other) -> unquote(other)
end
baz()
end
assert input |> expand() |> clean_meta([:export_vars, :generated]) == result
assert expand(quote(do: (with({foo} <- {bar}, do: baz = foo); baz))) ==
quote(do: (with({foo} <- {bar()}, do: baz = foo); baz()))
end
test "variables inside else do not leak" do
input = quote(do: (with({foo} <- {bar}, do: :ok, else: (baz -> baz)); baz))
other = Macro.var(:other, :elixir_with)
return = Macro.var(:return, :elixir_with)
result = quote do
case(case {bar()} do
{foo} -> {:ok, :ok}
unquote(other) -> {:error, unquote(other)}
end) do
{:ok, unquote(return)} -> unquote(return)
{:error, baz} -> baz
{:error, unquote(other)} -> :erlang.error({:with_clause, unquote(other)})
end
baz()
end
assert input |> expand() |> clean_meta([:export_vars, :generated]) == result
assert expand(quote(do: (with({foo} <- {bar}, do: :ok, else: (baz -> baz)); baz))) ==
quote(do: (with({foo} <- {bar()}, do: :ok, else: (baz -> baz)); baz()))
end
test "fails if \"do\" is missing" do
@@ -1187,6 +1151,24 @@ defmodule Kernel.ExpansionTest do
end)
end
assert_raise CompileError, ~r"invalid pattern in match", fn ->
expand(quote do
x = &(&1)
case true do
x.(false) -> true
end
end)
end
assert_raise CompileError, ~r"invalid expression in guard", fn ->
expand(quote do
x = &(&1)
case true do
true when x.(true) -> true
end
end)
end
assert_raise CompileError, ~r"invalid call foo\(1\)\(2\)", fn ->
expand(quote do: foo(1)(2))
end
@@ -15,10 +15,12 @@ defmodule Kernel.MacrosTest do
Kernel.MacrosTest.Nested = require Kernel.MacrosTest.Nested, as: Nested
@spec my_macro :: Macro.t
defmacro my_macro do
quote do: 1 + 1
end
@spec my_private_macro :: Macro.t
defmacrop my_private_macro do
quote do: 1 + 3
end
@@ -101,27 +101,6 @@ defmodule Kernel.OverridableExampleBehaviour do
end
defmodule Kernel.OverridableTest do
defmodule OverridableOrder do
def not_private(str) do
process_url(str)
end
def process_url(_str) do
:first
end
# There was a bug where the order in which we removed
# overridable expressions lead to errors. This module
# aims to guarantee removing process_url/1 before we
# remove the function that depends on it does not cause
# errors. If it compiles, it works!
defoverridable [process_url: 1, not_private: 1]
def process_url(_str) do
:second
end
end
require Kernel.Overridable, as: Overridable
use ExUnit.Case
@@ -130,6 +109,60 @@ defmodule Kernel.OverridableTest do
:code.delete(module)
end
test "overridable keeps function ordering" do
defmodule OverridableOrder do
def not_private(str) do
process_url(str)
end
def process_url(_str) do
:first
end
# There was a bug where the order in which we removed
# overridable expressions lead to errors. This module
# aims to guarantee removing process_url/1 before we
# remove the function that depends on it does not cause
# errors. If it compiles, it works!
defoverridable [process_url: 1, not_private: 1]
def process_url(_str) do
:second
end
end
end
test "overridable works with defaults" do
defmodule OverridableDefault do
def fun(value, opt \\ :from_parent) do
{value, opt}
end
defmacro macro(value, opt \\ :from_parent) do
{{value, opt}, Macro.escape(__CALLER__)}
end
# There was a bug where the default function would
# attempt to call its overridable name instead of
# func/1. If it compiles, it works!
defoverridable [fun: 1, fun: 2, macro: 1, macro: 2]
def fun(value) do
{value, super(value)}
end
defmacro macro(value) do
{{value, super(value)}, Macro.escape(__CALLER__)}
end
end
defmodule OverridableCall do
require OverridableDefault
OverridableDefault.fun(:foo)
OverridableDefault.macro(:bar)
end
end
test "overridable is made concrete if no other is defined" do
assert Overridable.sample == 1
end
+7 -7
View File
@@ -195,14 +195,14 @@ end
# DO NOT MOVE THIS LINE
defmodule Kernel.QuoteTest.Errors do
defmacro defadd do
defmacro defraise do
quote location: :keep do
def add(a, b), do: a + b
def will_raise(_a, _b), do: raise "oops"
end
end
defmacro will_raise do
quote location: :keep, do: raise "omg"
quote location: :keep, do: raise "oops"
end
end
@@ -211,16 +211,16 @@ defmodule Kernel.QuoteTest.ErrorsTest do
import Kernel.QuoteTest.Errors
# Defines the add function
defadd()
defraise()
test "inside function error" do
assert_raise ArithmeticError, fn ->
add(:a, :b)
assert_raise RuntimeError, fn ->
will_raise(:a, :b)
end
mod = Kernel.QuoteTest.ErrorsTest
file = __ENV__.file |> Path.relative_to_cwd |> String.to_charlist
assert [{^mod, :add, 2, [file: ^file, line: 200]} | _] = System.stacktrace
assert [{^mod, :will_raise, 2, [file: ^file, line: 200]} | _] = System.stacktrace
end
test "outside function error" do
+281 -246
View File
@@ -133,265 +133,300 @@ defmodule Kernel.RaiseTest do
end
end
test "rescue with underscore no exception" do
result = try do
RescueUndefinedModule.go
rescue
_ -> true
end
assert result
end
test "rescue with higher precedence than catch" do
result = try do
RescueUndefinedModule.go
catch
_, _ -> false
rescue
_ -> true
end
assert result
end
test "rescue runtime error" do
result = try do
raise "an exception"
rescue
RuntimeError -> true
catch
:error, _ -> false
end
assert result
result = try do
raise "an exception"
rescue
AnotherError -> true
catch
:error, _ -> false
end
refute result
end
test "rescue named runtime error" do
result = try do
raise "an exception"
rescue
x in [RuntimeError] -> Exception.message(x)
catch
:error, _ -> false
end
assert result == "an exception"
end
test "rescue argument error from elixir" do
result = try do
raise ArgumentError, ""
rescue
ArgumentError -> true
end
assert result
end
test "rescue named without aliases" do
result = try do
raise "an exception"
rescue
x -> Exception.message(x)
end
assert result == "an exception"
end
test "wrap custom Erlang error" do
result = try do
:erlang.error(:sample)
rescue
x in [RuntimeError, ErlangError] -> Exception.message(x)
end
assert result == "Erlang error: :sample"
end
test "undefined function error" do
result = try do
DoNotExist.for_sure()
rescue
x in [UndefinedFunctionError] -> Exception.message(x)
end
assert result == "function DoNotExist.for_sure/0 is undefined (module DoNotExist is not available)"
end
test "function clause error" do
result = try do
zero(1)
rescue
x in [FunctionClauseError] -> Exception.message(x)
end
assert result == "no function clause matching in Kernel.RaiseTest.zero/1"
end
test "badarg error" do
result = try do
:erlang.error(:badarg)
rescue
x in [ArgumentError] -> Exception.message(x)
end
assert result == "argument error"
end
test "tuple badarg error" do
result = try do
:erlang.error({:badarg, [1, 2, 3]})
rescue
x in [ArgumentError] -> Exception.message(x)
end
assert result == "argument error: [1, 2, 3]"
end
test "badarith error" do
result = try do
:erlang.error(:badarith)
rescue
x in [ArithmeticError] -> Exception.message(x)
end
assert result == "bad argument in arithmetic expression"
end
test "badarity error" do
fun = fn(x) -> x end
string = "#{inspect(fun)} with arity 1 called with 2 arguments (1, 2)"
result = try do
fun.(1, 2)
rescue
x in [BadArityError] -> Exception.message(x)
end
assert result == string
end
test "badfun error" do
# Avoid "invalid function call" warning in >= OTP 19
x = fn -> :example end
result = try do
x.().(2)
rescue
x in [BadFunctionError] -> Exception.message(x)
end
assert result == "expected a function, got: :example"
end
test "badmatch error" do
x = :example
result = try do
^x = zero(0)
rescue
x in [MatchError] -> Exception.message(x)
end
assert result == "no match of right hand side value: 0"
end
test "bad key error" do
result = try do
%{%{} | foo: :bar}
rescue
x in [KeyError] -> Exception.message(x)
end
assert result == "key :foo not found"
result = try do
%{}.foo
rescue
x in [KeyError] -> Exception.message(x)
end
assert result == "key :foo not found in: %{}"
end
test "bad map error" do
result = try do
%{zero(0) | foo: :bar}
rescue
x in [BadMapError] -> Exception.message(x)
end
assert result == "expected a map, got: 0"
end
test "bad boolean error" do
result = try do
1 and true
rescue
x in [BadBooleanError] -> Exception.message(x)
end
assert result == "expected a boolean on left-side of \"and\", got: 1"
end
test "case clause error" do
x = :example
result = try do
case zero(0) do
^x -> nil
describe "rescue" do
test "runtime error" do
result = try do
raise "an exception"
rescue
RuntimeError -> true
catch
:error, _ -> false
end
rescue
x in [CaseClauseError] -> Exception.message(x)
end
assert result == "no case clause matching: 0"
end
assert result
test "cond clause error" do
result = try do
cond do
!zero(0) -> :ok
result = try do
raise "an exception"
rescue
AnotherError -> true
catch
:error, _ -> false
end
rescue
x in [CondClauseError] -> Exception.message(x)
refute result
end
assert result == "no cond clause evaluated to a true value"
end
test "try clause error" do
f = fn() -> :example end
result = try do
try do
f.()
else
:other ->
:ok
test "named runtime error" do
result = try do
raise "an exception"
rescue
x in [RuntimeError] -> Exception.message(x)
catch
:error, _ -> false
end
rescue
x in [TryClauseError] -> Exception.message(x)
assert result == "an exception"
end
assert result == "no try clause matching: :example"
test "with higher precedence than catch" do
result = try do
raise "an exception"
catch
_, _ -> false
rescue
_ -> true
end
assert result
end
test "argument error from erlang" do
result = try do
:erlang.error(:badarg)
rescue
ArgumentError -> true
end
assert result
end
test "argument error from elixir" do
result = try do
raise ArgumentError, ""
rescue
ArgumentError -> true
end
assert result
end
test "catch-all variable" do
result = try do
raise "an exception"
rescue
x -> Exception.message(x)
end
assert result == "an exception"
end
test "catch-all underscore" do
result = try do
raise "an exception"
rescue
_ -> true
end
assert result
end
test "catch-all unused variable" do
result = try do
raise "an exception"
rescue
_any -> true
end
assert result
end
test "catch-all with \"x in _\" syntax" do
result = try do
raise "an exception"
rescue
exception in _ ->
Exception.message(exception)
end
assert result == "an exception"
end
end
test "undefined function error as Erlang error" do
result = try do
DoNotExist.for_sure()
rescue
x in [ErlangError] -> Exception.message(x)
describe "normalize" do
test "wrap custom Erlang error" do
result = try do
:erlang.error(:sample)
rescue
x in [ErlangError] -> Exception.message(x)
end
assert result == "Erlang error: :sample"
end
assert result == "function DoNotExist.for_sure/0 is undefined (module DoNotExist is not available)"
test "undefined function error" do
result = try do
DoNotExist.for_sure()
rescue
x in [UndefinedFunctionError] -> Exception.message(x)
end
assert result == "function DoNotExist.for_sure/0 is undefined (module DoNotExist is not available)"
end
test "function clause error" do
result = try do
zero(1)
rescue
x in [FunctionClauseError] -> Exception.message(x)
end
assert result == "no function clause matching in Kernel.RaiseTest.zero/1"
end
test "badarg error" do
result = try do
:erlang.error(:badarg)
rescue
x in [ArgumentError] -> Exception.message(x)
end
assert result == "argument error"
end
test "tuple badarg error" do
result = try do
:erlang.error({:badarg, [1, 2, 3]})
rescue
x in [ArgumentError] -> Exception.message(x)
end
assert result == "argument error: [1, 2, 3]"
end
test "badarith error" do
result = try do
:erlang.error(:badarith)
rescue
x in [ArithmeticError] -> Exception.message(x)
end
assert result == "bad argument in arithmetic expression"
end
test "badarity error" do
fun = fn(x) -> x end
string = "#{inspect(fun)} with arity 1 called with 2 arguments (1, 2)"
result = try do
fun.(1, 2)
rescue
x in [BadArityError] -> Exception.message(x)
end
assert result == string
end
test "badfun error" do
# Avoid "invalid function call" warning in >= OTP 19
x = fn -> :example end
result = try do
x.().(2)
rescue
x in [BadFunctionError] -> Exception.message(x)
end
assert result == "expected a function, got: :example"
end
test "badmatch error" do
x = :example
result = try do
^x = zero(0)
rescue
x in [MatchError] -> Exception.message(x)
end
assert result == "no match of right hand side value: 0"
end
test "bad key error" do
result = try do
%{%{} | foo: :bar}
rescue
x in [KeyError] -> Exception.message(x)
end
assert result == "key :foo not found"
result = try do
%{}.foo
rescue
x in [KeyError] -> Exception.message(x)
end
assert result == "key :foo not found in: %{}"
end
test "bad map error" do
result = try do
%{zero(0) | foo: :bar}
rescue
x in [BadMapError] -> Exception.message(x)
end
assert result == "expected a map, got: 0"
end
test "bad boolean error" do
result = try do
1 and true
rescue
x in [BadBooleanError] -> Exception.message(x)
end
assert result == "expected a boolean on left-side of \"and\", got: 1"
end
test "case clause error" do
x = :example
result = try do
case zero(0) do
^x -> nil
end
rescue
x in [CaseClauseError] -> Exception.message(x)
end
assert result == "no case clause matching: 0"
end
test "cond clause error" do
result = try do
cond do
!zero(0) -> :ok
end
rescue
x in [CondClauseError] -> Exception.message(x)
end
assert result == "no cond clause evaluated to a true value"
end
test "try clause error" do
f = fn() -> :example end
result = try do
try do
f.()
else
:other ->
:ok
end
rescue
x in [TryClauseError] -> Exception.message(x)
end
assert result == "no try clause matching: :example"
end
test "undefined function error as Erlang error" do
result = try do
DoNotExist.for_sure()
rescue
x in [ErlangError] -> Exception.message(x)
end
assert result == "function DoNotExist.for_sure/0 is undefined (module DoNotExist is not available)"
end
end
defmacrop exceptions do
+5 -5
View File
@@ -10,7 +10,7 @@ defmodule Kernel.WithTest do
test "matching with" do
assert with(_..42 <- 1..42, do: :ok) == :ok
assert with({:ok, res} <- :error, do: res) == :error
assert with({:ok, res} <- error(), do: res) == :error
assert with({:ok, _} = res <- ok(42), do: elem(res, 1)) == 42
end
@@ -29,7 +29,7 @@ defmodule Kernel.WithTest do
result = with({:ok, n1} <- ok(11), n2 <- 22, do: n1 + n2)
assert result == 33
result = with(n1 <- 11, {:ok, n2} <- :error, do: n1 + n2)
result = with(n1 <- 11, {:ok, n2} <- error(), do: n1 + n2)
assert result == :error
end
@@ -65,9 +65,9 @@ defmodule Kernel.WithTest do
end
test "else conditions" do
assert with({:ok, res} <- 41, do: res, else: ({:error, error} -> error; res -> res + 1)) == 42
assert with({:ok, res} <- 41, do: res, else: (res when res == 41 -> res + 1; res -> res)) == 42
assert with({:ok, res} <- 41, do: res, else: (_ -> :error)) == :error
assert with({:ok, res} <- four(), do: res, else: ({:error, error} -> error; res -> res + 1)) == 5
assert with({:ok, res} <- four(), do: res, else: (res when res == 4 -> res + 1; res -> res)) == 5
assert with({:ok, res} <- four(), do: res, else: (_ -> :error)) == :error
end
test "else conditions with match error" do
+6
View File
@@ -307,6 +307,12 @@ defmodule ModuleTest do
end
end
test "does not use ETS tables named after the module" do
in_module do
assert :ets.info(__MODULE__) == :undefined
end
end
## Definitions
test "defines?" do
+4
View File
@@ -32,6 +32,10 @@ defmodule PathTest do
File.rm_rf tmp_path("wildcard")
end
test "wildcard/2 raises on null byte" do
assert_raise ArgumentError, ~r/null byte/, fn -> Path.wildcard("foo\0bar") end
end
if windows?() do
describe "Windows" do
test "relative/1" do
+4
View File
@@ -377,6 +377,10 @@ defmodule Protocol.ConsolidationTest do
List.keyfind(docs, {:ok, 1}, 0)
end
test "consolidation keeps source" do
assert Sample.__info__(:compile)[:source]
end
test "consolidated keeps callbacks" do
callbacks = Kernel.Typespec.beam_callbacks(@sample_binary)
assert callbacks != []
+84
View File
@@ -69,6 +69,10 @@ defmodule RegistryTest do
[{self(), value}]
assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) ==
[{self(), value}]
assert Registry.match(registry, "hello", :_) ==
[{self(), value}]
assert Registry.match(registry, :_, :_) ==
[]
end
test "supports guard conditions", %{registry: registry} do
@@ -82,6 +86,42 @@ defmodule RegistryTest do
[{self(), value}]
end
test "unregister_match supports patterns", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
Registry.unregister_match(registry, "hello", {2, :_, :_})
assert Registry.lookup(registry, "hello") ==
[{self(), value}]
Registry.unregister_match(registry, "hello", {1.0, :_, :_})
assert Registry.lookup(registry, "hello") ==
[{self(), value}]
Registry.unregister_match(registry, "hello", {:_, :atom, :_})
assert Registry.lookup(registry, "hello") ==
[]
end
test "unregister_match supports guards", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}])
assert Registry.lookup(registry, "hello") ==
[]
end
test "unregister_match supports tricky keys", %{registry: registry} do
{:ok, _} = Registry.register(registry, :_, :foo)
{:ok, _} = Registry.register(registry, "hello", "b")
Registry.unregister_match(registry, :_, :foo)
assert Registry.lookup(registry, :_) ==
[]
assert Registry.keys(registry, self()) |> Enum.sort ==
["hello"]
end
test "compares using ===", %{registry: registry} do
{:ok, _} = Registry.register(registry, 1.0, :value)
{:ok, _} = Registry.register(registry, 1, :value)
@@ -347,6 +387,50 @@ defmodule RegistryTest do
[{self(), value1}, {self(), value2}]
end
test "unregister_match supports patterns", %{registry: registry} do
value1 = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value1)
value2 = {2, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value2)
Registry.unregister_match(registry, "hello", {2, :_, :_})
assert Registry.lookup(registry, "hello") ==
[{self(), value1}]
{:ok, _} = Registry.register(registry, "hello", value2)
Registry.unregister_match(registry, "hello", {2.0, :_, :_})
assert Registry.lookup(registry, "hello") ==
[{self(), value1}, {self(), value2}]
Registry.unregister_match(registry, "hello", {:_, :atom, :_})
assert Registry.lookup(registry, "hello") ==
[]
end
test "unregister_match supports guards", %{registry: registry} do
value1 = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value1)
value2 = {2, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value2)
Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}])
assert Registry.lookup(registry, "hello") ==
[{self(), value2}]
end
test "unregister_match supports tricky keys", %{registry: registry} do
{:ok, _} = Registry.register(registry, :_, :foo)
{:ok, _} = Registry.register(registry, :_, :bar)
{:ok, _} = Registry.register(registry, "hello", "a")
{:ok, _} = Registry.register(registry, "hello", "b")
Registry.unregister_match(registry, :_, :foo)
assert Registry.lookup(registry, :_) ==
[{self(), :bar}]
assert Registry.keys(registry, self()) |> Enum.sort ==
[:_, "hello", "hello"]
end
@tag listener: :"duplicate_listener_#{partitions}"
test "allows listeners", %{registry: registry, listener: listener} do
Process.register(self(), listener)
+50 -21
View File
@@ -59,22 +59,39 @@ defmodule StreamTest do
[[1, 2, 3, 4], [5, 6, 7, 8]]
end
test "chunk/4 is zippable" do
stream = Stream.chunk([1, 2, 3, 4, 5, 6], 3, 2, [])
test "chunk_every/2, chunk_every/3 and chunk_every/4" do
assert Stream.chunk_every([1, 2, 3, 4, 5], 2) |> Enum.to_list ==
[[1, 2], [3, 4], [5]]
assert Stream.chunk_every([1, 2, 3, 4, 5], 2, 2, [6]) |> Enum.to_list ==
[[1, 2], [3, 4], [5, 6]]
assert Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, :discard) |> Enum.to_list ==
[[1, 2, 3], [3, 4, 5]]
assert Stream.chunk_every([1, 2, 3, 4, 5, 6], 2, 3, :discard) |> Enum.to_list ==
[[1, 2], [4, 5]]
assert Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, []) |> Enum.to_list ==
[[1, 2, 3], [3, 4, 5], [5, 6]]
assert Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list ==
[[1, 2, 3], [4, 5, 6]]
assert Stream.chunk_every([1, 2, 3, 4, 5], 4, 4, 6..10) |> Enum.to_list ==
[[1, 2, 3, 4], [5, 6, 7, 8]]
end
test "chunk_every/4 is zippable" do
stream = Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, [])
list = Enum.to_list(stream)
assert Enum.zip(list, list) == Enum.zip(stream, stream)
end
test "chunk/4 is haltable" do
assert 1..10 |> Stream.take(6) |> Stream.chunk(4, 4, [7, 8]) |> Enum.to_list ==
test "chunk_every/4 is haltable" do
assert 1..10 |> Stream.take(6) |> Stream.chunk_every(4, 4, [7, 8]) |> Enum.to_list ==
[[1, 2, 3, 4], [5, 6, 7, 8]]
assert 1..10 |> Stream.take(6) |> Stream.chunk(4, 4, [7, 8]) |> Stream.take(3) |> Enum.to_list ==
assert 1..10 |> Stream.take(6) |> Stream.chunk_every(4, 4, [7, 8]) |> Stream.take(3) |> Enum.to_list ==
[[1, 2, 3, 4], [5, 6, 7, 8]]
assert 1..10 |> Stream.take(6) |> Stream.chunk(4, 4, [7, 8]) |> Stream.take(2) |> Enum.to_list ==
assert 1..10 |> Stream.take(6) |> Stream.chunk_every(4, 4, [7, 8]) |> Stream.take(2) |> Enum.to_list ==
[[1, 2, 3, 4], [5, 6, 7, 8]]
assert 1..10 |> Stream.take(6) |> Stream.chunk(4, 4, [7, 8]) |> Stream.take(1) |> Enum.to_list ==
assert 1..10 |> Stream.take(6) |> Stream.chunk_every(4, 4, [7, 8]) |> Stream.take(1) |> Enum.to_list ==
[[1, 2, 3, 4]]
assert 1..6 |> Stream.take(6) |> Stream.chunk(4, 4, [7, 8]) |> Enum.to_list ==
assert 1..6 |> Stream.take(6) |> Stream.chunk_every(4, 4, [7, 8]) |> Enum.to_list ==
[[1, 2, 3, 4], [5, 6, 7, 8]]
end
@@ -86,7 +103,7 @@ defmodule StreamTest do
[[1], [2, 2], [3], [4, 4, 6], [7, 7]]
assert stream |> Stream.take(3) |> Enum.to_list ==
[[1], [2, 2], [3]]
assert 1..10 |> Stream.chunk(2) |> Enum.take(2) ==
assert 1..10 |> Stream.chunk_every(2) |> Enum.take(2) ==
[[1, 2], [3, 4]]
end
@@ -96,12 +113,15 @@ defmodule StreamTest do
assert Enum.zip(list, list) == Enum.zip(stream, stream)
end
test "chunk_by/4" do
test "chunk_while/4" do
chunk_fun = fn i, acc ->
if rem(i, 2) == 0 do
{:cont, Enum.reverse([i | acc]), []}
else
{:cont, [i | acc]}
cond do
i > 10 ->
{:halt, acc}
rem(i, 2) == 0 ->
{:cont, Enum.reverse([i | acc]), []}
true ->
{:cont, [i | acc]}
end
end
@@ -110,15 +130,16 @@ defmodule StreamTest do
acc -> {:cont, Enum.reverse(acc), []}
end
stream = Stream.chunk_by(1..10, [], chunk_fun, after_fun)
assert lazy?(stream)
assert 1..10 |> Stream.chunk_by([], chunk_fun, after_fun) |> Enum.to_list() ==
assert Stream.chunk_while([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [], chunk_fun, after_fun) |> Enum.to_list ==
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert 0..10 |> Stream.chunk_by([], chunk_fun, after_fun) |> Enum.to_list() ==
assert Stream.chunk_while(0..9, [], chunk_fun, after_fun) |> Enum.to_list ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9]]
assert Stream.chunk_while(0..10, [], chunk_fun, after_fun) |> Enum.to_list ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert 0..11 |> Stream.chunk_by([], chunk_fun, after_fun) |> Enum.to_list() ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]
assert Stream.chunk_while(0..11, [], chunk_fun, after_fun) |> Enum.to_list ==
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
assert Stream.chunk_while([5, 7, 9, 11], [], chunk_fun, after_fun) |> Enum.to_list ==
[[5, 7, 9]]
end
test "concat/1" do
@@ -168,6 +189,14 @@ defmodule StreamTest do
stream = Stream.cycle([1, 2, 3])
assert is_function(stream)
assert_raise ArgumentError, "cannot cycle over empty enumerable", fn ->
Stream.cycle([])
end
assert_raise ArgumentError, "cannot cycle over empty enumerable", fn ->
Stream.cycle(%{}) |> Enum.to_list()
end
assert Stream.cycle([1, 2, 3]) |> Stream.take(5) |> Enum.to_list == [1, 2, 3, 1, 2]
assert Enum.take(stream, 5) == [1, 2, 3, 1, 2]
end
+113
View File
@@ -248,4 +248,117 @@ defmodule StringIOTest do
assert IO.stream(pid, 2) |> Enum.to_list == ["ab", "c"]
assert contents(pid) == {"", ""}
end
defp get_until(pid, encoding, prompt, module, function, extra_args \\ []) do
:io.request(pid, {:get_until, encoding, prompt, module, function, extra_args})
end
defmodule GetUntilCallbacks do
def until_eof(continuation, :eof) do
{:done, continuation, :eof}
end
def until_eof(continuation, content) do
{:more, continuation ++ content}
end
def until_eof_then_try_more('magic-stop-prefix' ++ continuation, :eof) do
{:done, continuation, :eof}
end
def until_eof_then_try_more(continuation, :eof) do
{:more, 'magic-stop-prefix' ++ continuation}
end
def until_eof_then_try_more(continuation, content) do
{:more, continuation ++ content}
end
def up_to_3_bytes(continuation, :eof) do
{:done, continuation, :eof}
end
def up_to_3_bytes(continuation, content) do
case continuation ++ content do
[a, b, c | tail] -> {:done, [a, b, c], tail}
str -> {:more, str}
end
end
def up_to_3_bytes_discard_rest(continuation, :eof) do
{:done, continuation, :eof}
end
def up_to_3_bytes_discard_rest(continuation, content) do
case continuation ++ content do
[a, b, c | _tail] -> {:done, [a, b, c], :eof}
str -> {:more, str}
end
end
end
test "get_until with up_to_3_bytes" do
pid = start("abcdefg")
result = get_until(pid, :unicode, "", GetUntilCallbacks, :up_to_3_bytes)
assert result == "abc"
assert IO.read(pid, :all) == "defg"
end
test "get_until with up_to_3_bytes_discard_rest" do
pid = start("abcdefg")
result = get_until(pid, :unicode, "", GetUntilCallbacks, :up_to_3_bytes_discard_rest)
assert result == "abc"
assert IO.read(pid, :all) == ""
end
test "get_until with until_eof" do
pid = start("abc\nd")
result = get_until(pid, :unicode, "", GetUntilCallbacks, :until_eof)
assert result == "abc\nd"
end
test "get_until with until_eof and \\r\\n" do
pid = start("abc\r\nd")
result = get_until(pid, :unicode, "", GetUntilCallbacks, :until_eof)
assert result == "abc\r\nd"
end
test "get_until with until_eof capturing prompt" do
pid = start("abc\nd", capture_prompt: true)
result = get_until(pid, :unicode, ">", GetUntilCallbacks, :until_eof)
assert result == "abc\nd"
assert StringIO.contents(pid) == {"", ">>>"}
end
test "get_until with until_eof_then_try_more" do
pid = start("abc\nd")
result = get_until(pid, :unicode, "", GetUntilCallbacks, :until_eof_then_try_more)
assert result == "abc\nd"
end
test "get_until with invalid UTF-8" do
pid = start(<<130, 227, 129, 132, 227, 129, 134>>)
result = get_until(pid, :unicode, "", GetUntilCallbacks, :until_eof)
assert result == :error
end
test "get_until with raw bytes (latin1)" do
pid = start(<<181, 255, 194, ?\n>>)
result = get_until(pid, :latin1, "", GetUntilCallbacks, :until_eof)
assert result == <<181, 255, 194, ?\n>>
end
test ":io.erl_scan_form/2" do
pid = start("1.")
result = :io.scan_erl_form(pid, 'p>')
assert result == {:ok, [{:integer, 1, 1}, {:dot, 1}], 1}
assert StringIO.contents(pid) == {"", ""}
end
test ":io.erl_scan_form/2 with capture_prompt" do
pid = start("1.", capture_prompt: true)
result = :io.scan_erl_form(pid, 'p>')
assert result == {:ok, [{:integer, 1, 1}, {:dot, 1}], 1}
assert StringIO.contents(pid) == {"", "p>p>"}
end
end
+16 -3
View File
@@ -73,6 +73,16 @@ defmodule SystemTest do
System.put_env(%{@test_var => "OTHER_SAMPLE"})
assert System.get_env(@test_var) == "OTHER_SAMPLE"
assert_raise ArgumentError, ~r[cannot execute System.put_env/2 for key with \"=\"], fn ->
System.put_env("FOO=BAR", "BAZ")
end
end
test "cmd/2 raises for null bytes" do
assert_raise ArgumentError, ~r"cannot execute System.cmd/3 for program with null byte", fn ->
System.cmd("null\0byte", [])
end
end
if windows?() do
@@ -107,11 +117,11 @@ defmodule SystemTest do
File.rm_rf! Path.dirname(tmp_path(@echo))
end
else
test "cmd/2 Unix" do
test "cmd/2 unix" do
assert {"hello\n", 0} = System.cmd "echo", ["hello"]
end
test "cmd/3 (with options) Unix" do
test "cmd/3 (with options) unix" do
assert {["hello\n"], 0} = System.cmd "echo", ["hello"],
into: [], cd: System.cwd!, env: %{"foo" => "bar", "baz" => nil},
arg0: "echo", stderr_to_stdout: true, parallelism: true
@@ -119,7 +129,7 @@ defmodule SystemTest do
@echo "echo-elixir-test"
test "cmd/2 with absolute and relative paths Unix" do
test "cmd/2 with absolute and relative paths ynix" do
echo = tmp_path(@echo)
File.mkdir_p! Path.dirname(echo)
File.cp! System.find_executable("echo"), echo
@@ -143,6 +153,9 @@ defmodule SystemTest do
assert System.find_executable("erl")
assert is_binary System.find_executable("erl")
assert !System.find_executable("does-not-really-exist-from-elixir")
assert_raise ArgumentError, ~r"cannot execute System.find_executable/1 for program with null byte", fn ->
System.find_executable("null\0byte")
end
end
test "monotonic_time/0" do
+6
View File
@@ -222,3 +222,9 @@ optimized_oror_test() ->
no_after_in_try_test() ->
{'try', _, [_], [_], _, []} = to_erl("try do :foo.bar() else _ -> :ok end").
optimized_inspect_interpolation_test() ->
{bin, _,
[{bin_element, _,
{call, _, {remote, _,{atom, _, 'Elixir.Kernel'}, {atom, _, inspect}}, [_]},
default, [binary]}]} = to_erl("\"#{inspect(1)}\"").
+13 -10
View File
@@ -70,15 +70,15 @@ defmodule String.Casing do
# Downcase
def downcase(string), do: downcase(string, "")
def downcase(string) when is_binary(string), do: downcase(string, "")
for {codepoint, _upper, lower, _title} <- codes, lower && lower != codepoint do
defp downcase(unquote(codepoint) <> rest, acc) do
defp downcase(<<unquote(codepoint), rest::bits>>, acc) do
downcase(rest, acc <> unquote(lower))
end
end
defp downcase(<<char, rest::binary>>, acc) do
defp downcase(<<char, rest::bits>>, acc) do
downcase(rest, <<acc::binary, char>>)
end
@@ -86,15 +86,15 @@ defmodule String.Casing do
# Upcase
def upcase(string), do: upcase(string, "")
def upcase(string) when is_binary(string), do: upcase(string, "")
for {codepoint, upper, _lower, _title} <- codes, upper && upper != codepoint do
defp upcase(unquote(codepoint) <> rest, acc) do
defp upcase(<<unquote(codepoint), rest::bits>>, acc) do
upcase(rest, acc <> unquote(upper))
end
end
defp upcase(<<char, rest::binary>>, acc) do
defp upcase(<<char, rest::bits>>, acc) do
upcase(rest, <<acc::binary, char>>)
end
@@ -140,11 +140,14 @@ defmodule String.Break do
# trim_leading
for codepoint <- whitespace do
def trim_leading(unquote(codepoint) <> rest), do: trim_leading(rest)
def trim_leading(string) when is_binary(string) do
do_trim_leading(string)
end
def trim_leading(""), do: ""
def trim_leading(string) when is_binary(string), do: string
for codepoint <- whitespace do
def do_trim_leading(<<unquote(codepoint), rest::bits>>), do: do_trim_leading(rest)
end
def do_trim_leading(<<rest::bits>>), do: rest
# trim_trailing
+13 -13
View File
@@ -147,7 +147,6 @@ defmodule ExUnit do
{:ok, _} = Application.ensure_all_started(:ex_unit)
configure(options)
config = put_defaults(configuration())
if Application.fetch_env!(:ex_unit, :autorun) do
Application.put_env(:ex_unit, :autorun, false)
@@ -155,6 +154,7 @@ defmodule ExUnit do
System.at_exit fn
0 ->
time = ExUnit.Server.cases_loaded()
config = persist_defaults(configuration())
%{failures: failures} = ExUnit.Runner.run(config, time)
System.at_exit fn _ ->
if failures > 0, do: exit({:shutdown, 1})
@@ -212,7 +212,8 @@ defmodule ExUnit do
* `:timeout` - sets the timeout for the tests, defaults to `60_000` milliseconds;
* `:trace` - sets ExUnit into trace mode, this sets `:max_cases` to `1` and
prints each test case and test while running.
prints each test case and test while running. Note that in trace mode test timeouts
will be ignored.
"""
def configure(options) do
@@ -226,6 +227,8 @@ defmodule ExUnit do
"""
def configuration do
Application.get_all_env(:ex_unit)
|> put_seed()
|> put_max_cases()
end
@doc """
@@ -260,28 +263,25 @@ defmodule ExUnit do
of failures and the number of skipped tests.
"""
def run do
config = put_defaults(configuration())
config = persist_defaults(configuration())
ExUnit.Runner.run(config, nil)
end
defp put_defaults(opts) do
opts
|> put_seed()
|> put_max_cases()
# Persists default values in application
# environment before the test suite starts.
defp persist_defaults(config) do
config |> Keyword.take([:seed, :max_cases]) |> configure()
config
end
defp put_seed(opts) do
Keyword.put_new_lazy(opts, :seed, fn ->
seed = :os.timestamp |> elem(2)
Application.put_env(:ex_unit, :seed, seed)
seed
:os.timestamp |> elem(2)
end)
end
defp put_max_cases(opts) do
max_cases = max_cases(opts)
Application.put_env(:ex_unit, :max_cases, max_cases)
Keyword.put(opts, :max_cases, max_cases)
Keyword.put(opts, :max_cases, max_cases(opts))
end
defp max_cases(opts) do
+10 -3
View File
@@ -23,7 +23,7 @@ defmodule ExUnit.DocTest do
To run doctests include them in an ExUnit case with a `doctest` macro:
defmodule MyModule.Test do
defmodule MyModuleTest do
use ExUnit.Case, async: true
doctest MyModule
end
@@ -193,8 +193,8 @@ defmodule ExUnit.DocTest do
end
end
tests = quote bind_quoted: binding() do
file = "(for doctest at) " <> Path.relative_to_cwd(mod.__info__(:compile)[:source])
tests = quote bind_quoted: [mod: mod, opts: opts] do
file = ExUnit.DocTest.__file__(mod)
for {name, test} <- ExUnit.DocTest.__doctests__(mod, opts) do
@tag :doctest
@file file
@@ -205,6 +205,13 @@ defmodule ExUnit.DocTest do
[require, tests]
end
@doc false
def __file__(module) do
source = module.__info__(:compile)[:source] ||
raise "#{inspect module} does not have compile-time source information"
"(for doctest at) " <> Path.relative_to_cwd(source)
end
@doc false
def __doctests__(module, opts) do
do_import = Keyword.get(opts, :import, false)
+224 -98
View File
@@ -2,10 +2,7 @@ defmodule IEx do
@moduledoc ~S"""
Elixir's interactive shell.
This module is the main entry point for Interactive Elixir and
in this documentation we will talk a bit about how IEx works.
Notice that some of the functionalities described here will not be available
Some of the functionalities described here will not be available
depending on your terminal. In particular, if you get a message
saying that the smart terminal could not be run, some of the
features described here won't work.
@@ -47,12 +44,78 @@ defmodule IEx do
set ERL_AFLAGS "-kernel shell_history enabled"
## Expressions in IEx
As an interactive shell, IEx evaluates expressions. This has some
interesting consequences that are worth discussing.
The first one is that the code is truly evaluated and not compiled.
This means that any benchmarking done in the shell is going to have
skewed results. So never run any profiling nor benchmarks in the shell.
Second, IEx allows you to break an expression into many lines,
since this is common in Elixir. For example:
iex(1)> "ab
...(1)> c"
"ab\nc"
In the example above, the shell will be expecting more input until it
finds the closing quote. Sometimes it is not obvious which character
the shell is expecting, and the user may find themselves trapped in
the state of incomplete expression with no ability to terminate it other
than by exiting the shell.
For such cases, there is a special break-trigger (`#iex:break`) that when
encountered on a line by itself will force the shell to break out of any
pending expression and return to its normal state:
iex(1)> ["ab
...(1)> c"
...(1)> "
...(1)> ]
...(1)> #iex:break
** (TokenMissingError) iex:1: incomplete expression
## The Break command
Inside IEx, hitting `Ctrl+C` will open up the `BREAK` menu. In this
menu you can quit the shell, see process and ets tables information
and much more.
## Exiting the shell
There are a few ways to quit the IEx shell:
* via the `BREAK` menu (available via `Ctrl+C`) by typing `q`, pressing enter
* by hitting `Ctrl+C`, `Ctrl+C`
* by hitting `Ctrl+\`
If you are connected to remote shell, it remains alive after disconnection.
## Prying and breakpoints
IEx also has the ability to set breakpoints on Elixir code and
"pry" into running processes. This allows the developer to have
an IEx session run inside a given function.
`IEx.pry/0` can be used when you are able to modify the source
code directly and recompile it:
def my_fun(arg1, arg2) do
require IEx; IEx.pry
... implementation ...
end
When the code is executed, it will ask you for permission to be
introspected.
Alternatively, you can use `IEx.break!/4` to setup a breakpoint
on a given module, function and arity you have no control of.
While `IEx.break!/4` is more flexible, it requires OTP 20+ and
it does not contain information about imports and aliases from
the source code.
## The User Switch command
Besides the break command, one can type `Ctrl+G` to get to the
@@ -85,8 +148,16 @@ defmodule IEx do
Since shells are isolated from each other, you can't access the
variables defined in one shell from the other one.
The user switch command menu also allows developers to connect to remote
shells using the `r` command. A topic which we will discuss next.
The User Switch command can also be used to terminate an existing
session, for example when the evaluator gets stuck in an infinite
loop or when you are stuck typing an expression:
User switch command
--> i
--> c
The user switch command menu also allows developers to connect to
remote shells using the `r` command. A topic which we will discuss next.
## Remote shells
@@ -209,48 +280,6 @@ defmodule IEx do
iex(1)> [1, 2, 3, 4, 5]
[1, 2, 3, ...]
## Expressions in IEx
As an interactive shell, IEx evaluates expressions. This has some
interesting consequences that are worth discussing.
The first one is that the code is truly evaluated and not compiled.
This means that any benchmarking done in the shell is going to have
skewed results. So never run any profiling nor benchmarks in the shell.
Second, IEx allows you to break an expression into many lines,
since this is common in Elixir. For example:
iex(1)> "ab
...(1)> c"
"ab\nc"
In the example above, the shell will be expecting more input until it
finds the closing quote. Sometimes it is not obvious which character
the shell is expecting, and the user may find themselves trapped in
the state of incomplete expression with no ability to terminate it other
than by exiting the shell.
For such cases, there is a special break-trigger (`#iex:break`) that when
encountered on a line by itself will force the shell to break out of any
pending expression and return to its normal state:
iex(1)> ["ab
...(1)> c"
...(1)> "
...(1)> ]
...(1)> #iex:break
** (TokenMissingError) iex:1: incomplete expression
## Exiting the shell
There are a few ways to quit the IEx shell:
* via the `BREAK` menu (available via `Ctrl+C`) by typing `q`, `Enter`
* by hitting `Ctrl+C`, `Ctrl+C`
* by hitting `Ctrl+\`
If you are connected to remote shell, it remains alive after disconnection.
"""
@doc """
@@ -419,17 +448,23 @@ defmodule IEx do
Pries into the process environment.
This is useful for debugging a particular chunk of code
and inspect the state of a particular process. The process
is temporarily changed to trap exits (i.e. the process flag
`:trap_exit` is set to `true`) and has the `group_leader` changed
to support ANSI escape codes. Those values are reverted by
calling `respawn/0`, which starts a new IEx shell, freeing up
the pried one.
when executed by a particular process. The process becomes
the evaluator of IEx commands and is temporarily changed to
have a custom group leader. Those values are reverted by
calling `IEx.Helpers.respawn/0`, which starts a new IEx shell,
freeing up the pried one.
When a process is pried, all code runs inside IEx and, as
such, it is evaluated and cannot access private functions
of the module being pried. Module functions still need to be
accessed via `Mod.fun(args)`.
When a process is pried, all code runs inside IEx and has
access to all imports and aliases from the original code.
However, the code is evaluated and therefore cannot access
private functions of the module being pried. Module functions
still need to be accessed via `Mod.fun(args)`.
Alternatively, you can use `IEx.break!/4` to setup a breakpoint
on a given module, function and arity you have no control of.
While `IEx.break!/4` is more flexible, it requires OTP 20+ and
it does not contain information about imports and aliases from
the source code.
## Examples
@@ -440,12 +475,12 @@ defmodule IEx do
the process information. Let's see an example:
import Enum, only: [map: 2]
require IEx
defmodule Adder do
def add(a, b) do
c = a + b
IEx.pry
require IEx; IEx.pry
end
end
@@ -470,56 +505,147 @@ defmodule IEx do
Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
Setting variables or importing modules in IEx does not
affect the caller's environment (hence it is called `pry`).
affect the caller's environment. However, sending and
receiving messages will change the process state.
## Pry and mix test
To use `IEx.pry/0` during tests, you need to run Mix inside
`iex` and pass the `--trace` to `mix test` to avoid running
into timeouts:
iex -S mix test --trace
iex -S mix test path/to/file:line --trace
"""
defmacro pry(timeout \\ 5000) do
defmacro pry() do
quote do
IEx.pry(binding(), __ENV__, unquote(timeout))
IEx.Pry.pry(binding(), __ENV__)
end
end
@doc """
Callback for `IEx.pry/1`.
You can invoke this function directly when you are not able to invoke
`IEx.pry/1` as a macro. This function expects the binding (from
`Kernel.binding/0`), the environment (from `__ENV__/0`) and the timeout
(a sensible default is 5000).
Macro-based shortcut for `IEx.break!/4`.
"""
def pry(binding, env, timeout) do
opts = [binding: binding, dot_iex_path: "", env: env, prefix: "pry"]
meta = "#{inspect self()} at #{Path.relative_to_cwd(env.file)}:#{env.line}"
desc =
if File.regular?(env.file) do
parse_file(env)
else
""
defmacro break!(ast, stops \\ 1) do
with {:/, _, [call, arity]} when is_integer(arity) <- ast,
{mod, fun, []} <- Macro.decompose_call(call) do
quote do
IEx.break!(unquote(mod), unquote(fun), unquote(arity), unquote(stops))
end
res = IEx.Server.take_over("Request to pry #{meta}#{desc}", opts, timeout)
# We cannot use colors because IEx may be off.
case res do
{:error, :no_iex} ->
extra =
case :os.type do
{:win32, _} -> " If you are Windows, you may need to start IEx with the --werl flag."
_ -> ""
end
IO.puts :stdio, "Cannot pry #{meta}. Is an IEx shell running?" <> extra
else
_ ->
:ok
raise ArgumentError, "expected Mod.fun/arity, such as URI.parse/1, got: #{Macro.to_string(ast)}"
end
res
end
defp parse_file(env) do
lines =
env.file
|> File.stream!
|> Enum.slice(max(env.line - 3, 0), 5)
Enum.intersperse(["\n\n" | lines], " ")
@doc """
Sets up a breakpoint in `module`, `function` and `arity` with
the given number of `stops`.
This function will instrument the given module and load a new
version in memory with breakpoints at the given function and
arity. If the module is recompiled, all breakpoints are lost.
When a breakpoint is reached, IEx will ask if you want to `pry`
the given function and arity. In other words, this works similar
to `IEx.pry/0` as the running process becomes the evaluator of
IEx commands and is temporarily changed to have a custom group
leader. However, differently from `IEx.pry/0`, aliases and imports
from the source code won't be available in the shell.
IEx helpers includes many conveniences related to breakpoints.
Below they are listed with the full module, such as `IEx.Helpers.breaks/0`,
but remember it can be called directly as `breaks()` inside IEx.
They are:
* `IEx.Helpers.break!/2` - sets up a breakpoint for a given `Mod.fun/arity`
* `IEx.Helpers.break!/4` - sets up a breakpoint for the given module, function, arity
* `IEx.Helpers.breaks/0` - prints all breakpoints and their ids
* `IEx.Helpers.continue/0` - continues until the next breakpoint in the same shell
* `IEx.Helpers.open/0` - opens editor on the current breakpoint
* `IEx.Helpers.remove_breaks/0` - removes all breakpoints in all modules
* `IEx.Helpers.remove_breaks/1` - removes all breakpoints in a given module
* `IEx.Helpers.reset_break/1` - sets the number of stops on the given id to zero
* `IEx.Helpers.reset_break/3` - sets the number of stops on the given module, function, arity to zero
* `IEx.Helpers.respawn/0` - starts a new shell (breakpoints will ask for permission once more)
* `IEx.Helpers.whereami/1` - shows the current location
By default, the number of stops in a breakpoint is 1. Any follow-up
call won't stop the code execution unless another breakpoint is set.
Alternatively, the number of be increased by passing the `stops`
argument. `IEx.Helpers.reset_break/1` and `IEx.Helpers.reset_break/3`
can be used to reset the number back to zero. Note the module remains
"instrumented" even after all stops on all breakpoints are consumed.
You can remove the instrumentation in a given module by calling
`IEx.Helpers.remove_breaks/1` and on all modules by calling
`IEx.Helpers.remove_breaks/0`.
To exit a breakpoint, the developer can either invoke `continue()`,
which will block the shell until the next breakpoint is found or
the process terminates, or invoke `respawn()`, which starts a new IEx
shell, freeing up the pried one.
This functionality only works on Elixir code and requires OTP 20+.
## Examples
The following sets up a breakpoint on `URI.decode_query/2`:
IEx.break!(URI, :decode_query, 2)
This call will setup a breakpoint that stops once.
To set a breakpoint that will stop 10 times:
IEx.break!(URI, :decode_query, 2, 10)
`IEx.break!/2` is a convenience macro that allows breakpoints
to be given in the `Mod.fun/arity` format:
require IEx
IEx.break!(URI.decode_query/2)
Or to set a breakpoint that will stop 10 times:
IEx.break!(URI.decode_query/2, 10)
This function returns the breakpoint ID and will raise if there
is an error setting up the breakpoint.
## Breaks and mix test
To use `IEx.break!/4` during tests, you need to run Mix inside
`iex` and pass the `--trace` to `mix test` to avoid running
into timeouts:
iex -S mix test --trace
iex -S mix test path/to/file:line --trace
"""
def break!(module, function, arity, stops \\ 1) do
case IEx.Pry.break(module, function, arity, stops) do
{:ok, id} ->
id
{:error, kind} ->
message =
case kind do
:missing_debug_info ->
"module #{inspect module} was not compiled with debug_info"
:no_beam_file ->
"could not find .beam file for #{inspect module}"
:non_elixir_module ->
"module #{inspect module} was not written in Elixir"
:otp_20_is_required ->
"you are running on an earlier OTP version than OTP 20"
:outdated_debug_info ->
"module #{inspect module} was not compiled with the latest debug_info"
:recompilation_failed ->
"the module could not be compiled with breakpoints (likely an internal error)"
:unknown_function_arity ->
"unknown function/macro #{Exception.format_mfa(module, function, arity)}"
end
raise "could not set breakpoint, " <> message
end
end
## Callbacks
+2 -13
View File
@@ -4,18 +4,7 @@ defmodule IEx.App do
use Application
def start(_type, _args) do
tab = IEx.Config.new()
case Supervisor.start_link([IEx.Config], strategy: :one_for_one, name: IEx.Supervisor) do
{:ok, pid} ->
{:ok, pid, tab}
{:error, _} = error ->
IEx.Config.delete(tab)
error
end
end
def stop(tab) do
IEx.Config.delete(tab)
children = [IEx.Config, IEx.Pry]
Supervisor.start_link(children, strategy: :one_for_one, name: IEx.Supervisor)
end
end
+8 -8
View File
@@ -395,8 +395,8 @@ defmodule IEx.Autocomplete do
## Evaluator interface
defp imports_from_env(server) do
with evaluator when is_pid(evaluator) <- server.evaluator(),
env_fields = IEx.Evaluator.fields_from_env(evaluator, [:functions, :macros]),
with {evaluator, server} <- server.evaluator(),
env_fields = IEx.Evaluator.fields_from_env(evaluator, server, [:functions, :macros]),
%{functions: funs, macros: macros} <- env_fields do
Enum.flat_map(funs ++ macros, &elem(&1, 1))
else
@@ -405,8 +405,8 @@ defmodule IEx.Autocomplete do
end
defp aliases_from_env(server) do
with evaluator when is_pid(evaluator) <- server.evaluator,
%{aliases: aliases} <- IEx.Evaluator.fields_from_env(evaluator, [:aliases]) do
with {evaluator, server} <- server.evaluator(),
%{aliases: aliases} <- IEx.Evaluator.fields_from_env(evaluator, server, [:aliases]) do
aliases
else
_ -> []
@@ -414,17 +414,17 @@ defmodule IEx.Autocomplete do
end
defp variables_from_binding(hint, server) do
with evaluator when is_pid(evaluator) <- server.evaluator() do
IEx.Evaluator.variables_from_binding(evaluator, hint)
with {evaluator, server} <- server.evaluator() do
IEx.Evaluator.variables_from_binding(evaluator, server, hint)
else
_ -> []
end
end
defp value_from_binding(ast_node, server) do
with evaluator when is_pid(evaluator) <- server.evaluator(),
with {evaluator, server} <- server.evaluator(),
{var, map_key_path} <- extract_from_ast(ast_node, []) do
IEx.Evaluator.value_from_binding(evaluator, var, map_key_path)
IEx.Evaluator.value_from_binding(evaluator, server, var, map_key_path)
else
_ -> :error
end
+7 -17
View File
@@ -119,17 +119,7 @@ defmodule IEx.Config do
# Agent API
def start_link(_) do
Agent.start_link(__MODULE__, :handle_init, [@table], [name: @agent])
end
def new() do
tab = :ets.new(@table, [:named_table, :public])
true = :ets.insert_new(tab, [after_spawn: []])
tab
end
def delete(__MODULE__) do
:ets.delete(__MODULE__)
Agent.start_link(__MODULE__, :handle_init, [], name: @agent)
end
def after_spawn(fun) do
@@ -146,9 +136,10 @@ defmodule IEx.Config do
# Agent callbacks
def handle_init(tab) do
:public = :ets.info(tab, :protection)
tab
def handle_init do
:ets.new(@table, [:named_table, :public])
true = :ets.insert_new(@table, [after_spawn: []])
@table
end
def handle_after_spawn(tab, fun) do
@@ -164,10 +155,9 @@ defmodule IEx.Config do
end
defp update_configuration(config) do
put = fn({key, value}) when key in @keys ->
Enum.each(config, fn {key, value} when key in @keys ->
Application.put_env(:iex, key, value)
end
Enum.each(config, put)
end)
end
defp merge_option(:colors, old, new) when is_list(new), do: Keyword.merge(old, new)
+55 -45
View File
@@ -14,11 +14,11 @@ defmodule IEx.Evaluator do
old_leader = Process.group_leader
Process.group_leader(self(), leader)
state = loop_state(opts)
state = loop_state(server, IEx.History.init, opts)
command == :ack && :proc_lib.init_ack(self())
try do
loop(server, IEx.History.init, state)
loop(state)
after
Process.group_leader(self(), old_leader)
end
@@ -28,10 +28,10 @@ defmodule IEx.Evaluator do
Gets a value out of the binding, using the provided
variable name and map key path.
"""
@spec value_from_binding(pid, atom, [atom]) :: {:ok, any} | :error
def value_from_binding(evaluator, var_name, map_key_path) do
@spec value_from_binding(pid, pid, atom, [atom]) :: {:ok, any} | :error
def value_from_binding(evaluator, server, var_name, map_key_path) do
ref = make_ref()
send evaluator, {:value_from_binding, ref, self(), var_name, map_key_path}
send evaluator, {:value_from_binding, server, ref, self(), var_name, map_key_path}
receive do
{^ref, result} -> result
@@ -44,10 +44,10 @@ defmodule IEx.Evaluator do
Gets a list of variables out of the binding that match the passed
variable prefix.
"""
@spec variables_from_binding(pid, String.t) :: [String.t]
def variables_from_binding(evaluator, variable_prefix) do
@spec variables_from_binding(pid, pid, String.t) :: [String.t]
def variables_from_binding(evaluator, server, variable_prefix) do
ref = make_ref()
send evaluator, {:variables_from_binding, ref, self(), variable_prefix}
send evaluator, {:variables_from_binding, server, ref, self(), variable_prefix}
receive do
{^ref, result} -> result
@@ -59,10 +59,10 @@ defmodule IEx.Evaluator do
@doc """
Returns the named fields from the current session environment.
"""
@spec fields_from_env(pid, [atom]) :: %{atom => term}
def fields_from_env(evaluator, fields) do
@spec fields_from_env(pid, pid, [atom]) :: %{atom => term}
def fields_from_env(evaluator, server, fields) do
ref = make_ref()
send evaluator, {:fields_from_env, ref, self(), fields}
send evaluator, {:fields_from_env, server, ref, self(), fields}
receive do
{^ref, result} -> result
@@ -71,23 +71,23 @@ defmodule IEx.Evaluator do
end
end
defp loop(server, history, state) do
defp loop(%{server: server} = state) do
receive do
{:eval, ^server, code, iex_state} ->
{result, history, state} = eval(code, iex_state, history, state)
{result, state} = eval(code, iex_state, state)
send server, {:evaled, self(), result}
loop(server, history, state)
{:fields_from_env, ref, receiver, fields} ->
loop(state)
{:fields_from_env, ^server, ref, receiver, fields} ->
send receiver, {ref, Map.take(state.env, fields)}
loop(server, history, state)
{:value_from_binding, ref, receiver, var_name, map_key_path} ->
loop(state)
{:value_from_binding, ^server, ref, receiver, var_name, map_key_path} ->
value = traverse_binding(state.binding, var_name, map_key_path)
send receiver, {ref, value}
loop(server, history, state)
{:variables_from_binding, ref, receiver, var_prefix} ->
loop(state)
{:variables_from_binding, ^server, ref, receiver, var_prefix} ->
value = find_matched_variables(state.binding, var_prefix)
send receiver, {ref, value}
loop(server, history, state)
loop(state)
{:done, ^server} ->
:ok
end
@@ -110,18 +110,13 @@ defmodule IEx.Evaluator do
do: var_name
end
defp loop_state(opts) do
env =
if env = opts[:env] do
:elixir.env_for_eval(env, [])
else
:elixir.env_for_eval(file: "iex")
end
defp loop_state(server, history, opts) do
env = opts[:env] || :elixir.env_for_eval(file: "iex")
env = %{env | match_vars: :apply}
{_, _, env, scope} = :elixir.eval('import IEx.Helpers', [], env)
binding = Keyword.get(opts, :binding, [])
state = %{binding: binding, scope: scope, env: env}
state = %{binding: binding, scope: scope, env: env, server: server, history: history}
case opts[:dot_iex_path] do
"" -> state
@@ -178,34 +173,48 @@ defmodule IEx.Evaluator do
# https://github.com/elixir-lang/elixir/issues/1089 for discussion.
@break_trigger '#iex:break\n'
defp eval(code, iex_state, history, state) do
defp eval(code, iex_state, state) do
try do
do_eval(String.to_charlist(code), iex_state, history, state)
do_eval(String.to_charlist(code), iex_state, state)
catch
kind, error ->
print_error(kind, error, System.stacktrace)
{%{iex_state | cache: ''}, history, state}
{%{iex_state | cache: ''}, state}
end
end
defp do_eval(@break_trigger, %IEx.State{cache: ''} = iex_state, history, state) do
{iex_state, history, state}
defp do_eval(@break_trigger, %IEx.State{cache: ''} = iex_state, state) do
{iex_state, state}
end
defp do_eval(@break_trigger, iex_state, _history, _state) do
defp do_eval(@break_trigger, iex_state, _state) do
:elixir_errors.parse_error(iex_state.counter, "iex", "incomplete expression", "")
end
defp do_eval(latest_input, iex_state, history, state) do
defp do_eval(latest_input, iex_state, state) do
code = iex_state.cache ++ latest_input
line = iex_state.counter
Process.put(:iex_history, history)
handle_eval(Code.string_to_quoted(code, [line: line, file: "iex"]), code, line, iex_state, history, state)
put_history(state)
put_whereami(state)
quoted = Code.string_to_quoted(code, line: line, file: "iex")
handle_eval(quoted, code, line, iex_state, state)
after
Process.delete(:iex_history)
Process.delete(:iex_whereami)
end
defp handle_eval({:ok, forms}, code, line, iex_state, history, state) do
defp put_history(%{history: history}) do
Process.put(:iex_history, history)
end
defp put_whereami(%{env: %{file: "iex"}}) do
:ok
end
defp put_whereami(%{env: %{file: file, line: line}}) do
Process.put(:iex_whereami, {file, line})
end
defp handle_eval({:ok, forms}, code, line, iex_state, state) do
{result, binding, env, scope} =
:elixir.eval_forms(forms, state.binding, state.env, state.scope)
unless result == IEx.dont_display_result, do: io_inspect(result)
@@ -218,22 +227,23 @@ defmodule IEx.Evaluator do
scope: scope,
binding: binding}
{iex_state, update_history(history, line, code, result), state}
{iex_state, update_history(state, line, code, result)}
end
defp handle_eval({:error, {_, _, ""}}, code, _line, iex_state, history, state) do
defp handle_eval({:error, {_, _, ""}}, code, _line, iex_state, state) do
# Update iex_state.cache so that IEx continues to add new input to
# the unfinished expression in "code"
{%{iex_state | cache: code}, history, state}
{%{iex_state | cache: code}, state}
end
defp handle_eval({:error, {line, error, token}}, _code, _line, _iex_state, _, _state) do
defp handle_eval({:error, {line, error, token}}, _code, _line, _iex_state, _state) do
# Encountered malformed expression
:elixir_errors.parse_error(line, "iex", error, token)
end
defp update_history(history, counter, cache, result) do
IEx.History.append(history, {counter, cache, result}, IEx.Config.history_size)
defp update_history(state, counter, cache, result) do
history_size = IEx.Config.history_size
update_in(state.history, &IEx.History.append(&1, {counter, cache, result}, history_size))
end
defp io_inspect(result) do
+341 -121
View File
@@ -22,27 +22,28 @@ defmodule IEx.Helpers do
There are many other helpers available, here are some examples:
* `b/1` - prints callbacks info and docs for a given module
* `c/1` - compiles a file into the current directory
* `c/2` - compiles a file to the given path
* `cd/1` - changes the current directory
* `clear/0` - clears the screen
* `e/1` - shows all exports (functions + macros) in a module
* `flush/0` - flushes all messages sent to the shell
* `h/0` - prints this help message
* `h/1` - prints help for the given module, function or macro
* `i/0` - prints information about the last value
* `i/1` - prints information about the given term
* `ls/0` - lists the contents of the current directory
* `ls/1` - lists the contents of the specified directory
* `pid/1` - creates a PID from a string
* `pid/3` - creates a PID with the 3 integer arguments passed
* `pwd/0` - prints the current working directory
* `r/1` - recompiles the given module's source file
* `recompile/0` - recompiles the current project
* `respawn/0` - respawns the current shell
* `v/0` - retrieves the last value from the history
* `v/1` - retrieves the nth value from the history
* `b/1` - prints callbacks info and docs for a given module
* `c/1` - compiles a file into the current directory
* `c/2` - compiles a file to the given path
* `cd/1` - changes the current directory
* `clear/0` - clears the screen
* `exports/1` - shows all exports (functions + macros) in a module
* `flush/0` - flushes all messages sent to the shell
* `h/0` - prints this help message
* `h/1` - prints help for the given module, function or macro
* `i/0` - prints information about the last value
* `i/1` - prints information about the given term
* `ls/0` - lists the contents of the current directory
* `ls/1` - lists the contents of the specified directory
* `open/1` - opens the source for the given module or function in your editor
* `pid/1` - creates a PID from a string
* `pid/3` - creates a PID with the 3 integer arguments passed
* `pwd/0` - prints the current working directory
* `r/1` - recompiles the given module's source file
* `recompile/0` - recompiles the current project
* `runtime_info/0` - prints runtime info (versions, memory usage, stats)
* `v/0` - retrieves the last value from the history
* `v/1` - retrieves the nth value from the history
Help for all of those functions can be consulted directly from
the command line using the `h/1` helper itself. Try:
@@ -52,7 +53,10 @@ defmodule IEx.Helpers do
To list all IEx helpers available, which is effectively all
exports (functions and macros) in the `IEx.Helpers` module:
iex> e(IEx.Helpers)
iex> exports(IEx.Helpers)
This module also include helpers for debugging purposes, see
`IEx.break!/4` for more information.
To learn more about IEx as a whole, type `h(IEx)`.
"""
@@ -193,12 +197,74 @@ defmodule IEx.Helpers do
dont_display_result()
end
@doc """
Opens the current prying location.
This command only works inside a pry session started manually
via `IEx.pry/0` or a breakpoint set via `IEx.break!/4`. Calling
this function during a regular `IEx` session will print an error.
Keep in mind the `open/0` location may not exist when prying
precompiled source code, such as Elixir itself.
For more information and to open any module or function, see
`open/1`.
"""
def open() do
case Process.get(:iex_whereami) do
{file, line} ->
IEx.Introspection.open({file, line})
_ ->
IO.puts IEx.color(:eval_error, "Pry session is not currently enabled")
end
dont_display_result()
end
@doc """
Opens the given module, module/function/arity or `{file, line}`.
This function uses the `ELIXIR_EDITOR` environment variable
and falls back to `EDITOR` if the former is not available.
By default, it attempts to open the file and line using the
`file:line` notation. For example, if your editor is called
`subl`, it will open the file as:
subl path/to/file:line
Custom editors are supported by using the __FILE__ and __LINE__
notations. For example, vi/vim users can set `ELIXIR_EDITOR` to:
ELIXIR_EDITOR="vi +__LINE__ __FILE__"
and Elixir will properly interpolate values.
Since this function prints the result returned by the editor,
`ELIXIR_EDITOR` can be set "echo" if you prefer to display the
location rather than opening it.
Keep in mind the location may not exist when opening
precompiled source code, such as Elixir itself.
## Examples
iex> open MyApp
iex> open MyApp.fun/2
iex> open {"path/to/file", 1}
"""
defmacro open(term) do
quote do
IEx.Introspection.open(unquote(IEx.Introspection.decompose(term)))
end
end
@doc """
Prints the documentation for `IEx.Helpers`.
"""
def h() do
IEx.Introspection.h(IEx.Helpers)
dont_display_result()
end
@doc """
@@ -217,42 +283,9 @@ defmodule IEx.Helpers do
iex> h Enum.all?
"""
@h_modules [__MODULE__, Kernel, Kernel.SpecialForms]
defmacro h(term)
defmacro h({:/, _, [call, arity]} = term) do
args =
case Macro.decompose_call(call) do
{_mod, :__info__, []} when arity == 1 ->
[Module, :__info__, 1]
{mod, fun, []} ->
[mod, fun, arity]
{fun, []} ->
[@h_modules, fun, arity]
_ ->
[term]
end
defmacro h(term) do
quote do
IEx.Introspection.h(unquote_splicing(args))
end
end
defmacro h(call) do
args =
case Macro.decompose_call(call) do
{_mod, :__info__, []} ->
[Module, :__info__, 1]
{mod, fun, []} ->
[mod, fun]
{fun, []} ->
[@h_modules, fun]
_ ->
[call]
end
quote do
IEx.Introspection.h(unquote_splicing(args))
IEx.Introspection.h(unquote(IEx.Introspection.decompose(term)))
end
end
@@ -268,22 +301,9 @@ defmodule IEx.Helpers do
iex> b(Mix.Task.run)
iex> b(GenServer)
"""
defmacro b(term)
defmacro b({:/, _, [{{:., _, [mod, fun]}, _, []}, arity]}) do
defmacro b(term) do
quote do
IEx.Introspection.b(unquote(mod), unquote(fun), unquote(arity))
end
end
defmacro b({{:., _, [mod, fun]}, _, []}) do
quote do
IEx.Introspection.b(unquote(mod), unquote(fun))
end
end
defmacro b(module) do
quote do
IEx.Introspection.b(unquote(module))
IEx.Introspection.b(unquote(IEx.Introspection.decompose(term)))
end
end
@@ -305,22 +325,9 @@ defmodule IEx.Helpers do
@type t() :: Enumerable.t()
"""
defmacro t(term)
defmacro t({:/, _, [{{:., _, [mod, fun]}, _, []}, arity]}) do
defmacro t(term) do
quote do
IEx.Introspection.t(unquote(mod), unquote(fun), unquote(arity))
end
end
defmacro t({{:., _, [mod, fun]}, _, []}) do
quote do
IEx.Introspection.t(unquote(mod), unquote(fun))
end
end
defmacro t(module) do
quote do
IEx.Introspection.t(unquote(module))
IEx.Introspection.t(unquote(IEx.Introspection.decompose(term)))
end
end
@@ -336,30 +343,9 @@ defmodule IEx.Helpers do
iex> s(is_atom/1)
"""
defmacro s(term)
defmacro s({:/, _, [call, arity]} = term) do
args =
case Macro.decompose_call(call) do
{mod, fun, []} -> [mod, fun, arity]
{fun, []} -> [Kernel, fun, arity]
_ -> [term]
end
defmacro s(term) do
quote do
IEx.Introspection.s(unquote_splicing(args))
end
end
defmacro s(call) do
args =
case Macro.decompose_call(call) do
{mod, fun, []} -> [mod, fun]
{fun, []} -> [Kernel, fun]
_ -> [call]
end
quote do
IEx.Introspection.s(unquote_splicing(args))
IEx.Introspection.s(unquote(IEx.Introspection.decompose(term)))
end
end
@@ -497,6 +483,77 @@ defmodule IEx.Helpers do
|> Enum.map_join(", ", &inspect/1)
end
@doc """
Prints vm/runtime information such as versions, memory usage and statistics.
"""
def runtime_info do
print_pane("System and architecture")
print_entry("Elixir version", System.version)
print_entry("OTP version", :erlang.system_info(:otp_release))
print_entry("ERTS version", :erlang.system_info(:version))
print_entry("Compiled for", :erlang.system_info(:system_architecture))
print_entry("Schedulers", :erlang.system_info(:schedulers))
print_entry("Schedulers online", :erlang.system_info(:schedulers_online))
print_pane("Memory")
print_memory("Total", :total, :MB)
print_memory("Atoms", :atom)
print_memory("Binaries", :binary)
print_memory("Code", :code)
print_memory("ETS", :ets)
print_memory("Processes", :processes)
print_pane("Statistics / limits")
print_uptime()
print_entry("Run queue", :erlang.statistics(:run_queue))
if :erlang.system_info(:otp_release) >= '20' do
print_percentage("Atoms", :atom_count, :atom_limit)
end
print_percentage("ETS", :ets_count, :ets_limit)
print_percentage("Ports", :port_count, :port_limit)
print_percentage("Processes", :process_count, :process_limit)
IO.puts ""
dont_display_result()
end
defp print_pane(msg) do
IO.puts IEx.color(:eval_result, ["\n## ", msg, " \n"])
end
defp print_entry(_key, nil), do: :ok
defp print_entry(key, value), do: IO.puts "#{pad_key(key)}#{value}"
defp print_uptime() do
IO.write pad_key("Uptime")
:c.uptime()
end
defp print_percentage(key, min, max) do
min = get_stat(min)
max = get_stat(max)
percentage = trunc((min / max) * 100)
IO.puts "#{pad_key(key)}#{min} / #{max} (#{percentage}% used)"
end
defp get_stat(:ets_count), do: length(:ets.all())
defp get_stat(other), do: :erlang.system_info(other)
defp print_memory(key, memory, unit \\ :kB) do
value =
memory
|> :erlang.memory()
|> div(memory_unit(unit))
|> round()
IO.puts "#{pad_key(key)}#{value} #{unit}"
end
defp memory_unit(:MB), do: 1024 * 1024
defp memory_unit(:kB), do: 1024
defp pad_key(key), do: String.pad_trailing("#{key}:", 20, " ")
@doc """
Flushes all messages sent to the shell and prints them out.
"""
@@ -546,14 +603,14 @@ defmodule IEx.Helpers do
@doc """
Prints a list of all the functions and macros exported by the given module.
"""
def e(module \\ Kernel) do
IEx.Autocomplete.exports(module) |> print_exports()
dont_display_result()
end
defp print_exports(functions) do
list = Enum.map(functions, fn({name, arity}) -> Atom.to_string(name) <> "/" <> Integer.to_string(arity) end)
def exports(module \\ Kernel) do
exports = IEx.Autocomplete.exports(module)
list =
Enum.map(exports, fn {name, arity} ->
Atom.to_string(name) <> "/" <> Integer.to_string(arity)
end)
print_table(list)
dont_display_result()
end
@doc """
@@ -632,14 +689,177 @@ defmodule IEx.Helpers do
@doc """
Respawns the current shell by starting a new shell process.
Returns `true` if it worked.
"""
def respawn do
if whereis = IEx.Server.whereis do
send whereis, {:respawn, self()}
dont_display_result()
end
dont_display_result()
end
@doc """
Continues execution of the current process.
This is usually called by sessions started with `IEx.pry/0`
or `IEx.break!/4`. This allows the current to execute until
the next breakpoint, which will automatically yield control
back to IEx without requesting permission to pry.
If the running process terminates, a new IEx session is
started.
While the process executes, the user will no longer have
control of the shell. If you would rather start a new shell,
use `respawn/0` instead.
"""
def continue do
if whereis = IEx.Server.whereis do
send whereis, {:continue, self()}
end
dont_display_result()
end
@doc """
Macro-based shortcut for `IEx.break!/4`.
"""
defmacro break!(ast, stops \\ 1) do
quote do
require IEx
IEx.break!(unquote(ast), unquote(stops))
end
end
@doc """
Sets up a breakpoint in `module`, `function` and `arity`
with the given number of `stops`.
See `IEx.break!/4` for a complete description of breakpoints
in IEx.
"""
defdelegate break!(module, function, arity, stops \\ 1), to: IEx
@doc """
Prints all breakpoints to the terminal.
"""
def breaks do
breaks(IEx.Pry.breaks())
end
defp breaks([]) do
IO.puts IEx.color(:eval_info, "No breakpoints set")
dont_display_result()
end
defp breaks(breaks) do
entries =
for {id, module, {function, arity}, stops} <- breaks do
{Integer.to_string(id),
Exception.format_mfa(module, function, arity),
Integer.to_string(stops)}
end
entries = [{"ID", "Module.function/arity", "Pending stops"} | entries]
{id_max, mfa_max, stops_max} =
Enum.reduce(entries, {0, 0, 0}, fn {id, mfa, stops}, {id_max, mfa_max, stops_max} ->
{max(byte_size(id), id_max),
max(byte_size(mfa), mfa_max),
max(byte_size(stops), stops_max)}
end)
[header | entries] = entries
IO.puts ""
print_break(header, id_max, mfa_max)
IO.puts [String.duplicate("-", id_max + 2), ?\s,
String.duplicate("-", mfa_max + 2), ?\s,
String.duplicate("-", stops_max + 2)]
Enum.each(entries, &print_break(&1, id_max, mfa_max))
IO.puts ""
dont_display_result()
end
defp print_break({id, mfa, stops}, id_max, mfa_max) do
IO.puts [?\s, String.pad_trailing(id, id_max + 2),
?\s, String.pad_trailing(mfa, mfa_max + 2),
?\s, stops]
end
@doc """
Sets the number of pending stops in the breakpoint
with the given id to zero.
Returns `:ok` if there is such breakpoint id. `:not_found`
otherwise.
Note the module remains "instrumented" on reset. If you would
like to effectively remove all breakpoints and instrumentation
code from a module, use `remove_breaks/1` instead.
"""
defdelegate reset_break(id), to: IEx.Pry
@doc """
Sets the number of pending stops in the given module,
function and arity to zero.
If the module is not instrumented or if the given function
does not have a breakpoint, it is a no-op and it returns
`:not_found`. Otherwise it returns `:ok`.
Note the module remains "instrumented" on reset. If you would
like to effectively remove all breakpoints and instrumentation
code from a module, use `remove_breaks/1` instead.
"""
defdelegate reset_break(module, function, arity), to: IEx.Pry
@doc """
Removes all breakpoints and instrumentation from `module`.
"""
defdelegate remove_breaks(module), to: IEx.Pry
@doc """
Removes all breakpoints and instrumentation from all modules.
"""
defdelegate remove_breaks(), to: IEx.Pry
@doc """
Prints the current location in a pry session.
It expects a `radius` which chooses how many lines before and after
the current line we should print. By default the `radius` is of two
lines:
Location: lib/iex/lib/iex/helpers.ex:79
77:
78: def recompile do
79: require IEx; IEx.pry
80: if mix_started?() do
81: config = Mix.Project.config
This command only works inside a pry session started manually
via `IEx.pry/0` or a breakpoint set via `IEx.break!/4`. Calling
this function during a regular `IEx` session will print an error.
Keep in mind the `whereami/1` location may not exist when prying
precompiled source code, such as Elixir itself.
"""
def whereami(radius \\ 2) do
case Process.get(:iex_whereami) do
{file, line} ->
IO.puts IEx.color(:eval_info, ["Location: ", Path.relative_to_cwd(file), ":", Integer.to_string(line)])
case IEx.Pry.whereami(file, line, radius) do
{:ok, lines} ->
IO.write [?\n, lines, ?\n]
:error ->
IO.puts IEx.color(:eval_error, "Could not extract source snippet. Location is not available.")
end
_ ->
IO.puts IEx.color(:eval_error, "Pry session is not currently enabled")
end
dont_display_result()
end
@doc """
+233 -119
View File
@@ -1,5 +1,5 @@
# Convenience helpers for showing docs, specs and types
# from modules. Invoked directly from IEx.Helpers.
# Convenience helpers for showing docs, specs, types
# and opening modules. Invoked directly from IEx.Helpers.
defmodule IEx.Introspection do
@moduledoc false
@@ -8,7 +8,158 @@ defmodule IEx.Introspection do
alias Kernel.Typespec
@doc """
Prints the documentation for the given module.
Decomposes an introspection call into `{mod, fun, arity}`,
`{mod, fun}` or `mod`.
"""
@default_modules [IEx.Helpers, Kernel, Kernel.SpecialForms]
def decompose({:/, _, [call, arity]} = term) do
case Macro.decompose_call(call) do
{_mod, :__info__, []} when arity == 1 ->
{:{}, [], [Module, :__info__, 1]}
{mod, fun, []} ->
{:{}, [], [mod, fun, arity]}
{fun, []} ->
{:{}, [], [find_decompose_fun_arity(fun, arity), fun, arity]}
_ ->
term
end
end
def decompose(call) do
case Macro.decompose_call(call) do
{_mod, :__info__, []} ->
Macro.escape {Module, :__info__, 1}
{mod, fun, []} ->
{mod, fun}
{fun, []} ->
{find_decompose_fun(fun), fun}
_ ->
call
end
end
defp find_decompose_fun(fun) do
Enum.find(@default_modules, Kernel, fn mod ->
Keyword.has_key?(mod.__info__(:functions), fun) or
Keyword.has_key?(mod.__info__(:macros), fun)
end)
end
defp find_decompose_fun_arity(fun, arity) do
pair = {fun, arity}
Enum.find(@default_modules, Kernel, fn mod ->
pair in mod.__info__(:functions) or pair in mod.__info__(:macros)
end)
end
@doc """
Opens the given module, mfa, file/line, binary.
"""
def open(module) when is_atom(module) do
case open_mfa(module, :__info__, 1) do
{source, nil, _} -> open(source)
{_, tuple, _} -> open(tuple)
:error -> puts_error("Could not open: #{inspect module}. Module is not available.")
end
dont_display_result()
end
def open({module, function}) when is_atom(module) and is_atom(function) do
case open_mfa(module, function, :*) do
{_, _, nil} -> puts_error("Could not open: #{inspect module}.#{function}. Function/macro is not available.")
{_, _, tuple} -> open(tuple)
:error -> puts_error("Could not open: #{inspect module}.#{function}. Module is not available.")
end
dont_display_result()
end
def open({module, function, arity}) when is_atom(module) and is_atom(function) and is_integer(arity) do
case open_mfa(module, function, arity) do
{_, _, nil} -> puts_error("Could not open: #{inspect module}.#{function}/#{arity}. Function/macro is not available.")
{_, _, tuple} -> open(tuple)
:error -> puts_error("Could not open: #{inspect module}.#{function}/#{arity}. Module is not available.")
end
dont_display_result()
end
def open({file, line}) when is_binary(file) and is_integer(line) do
cond do
not File.regular?(file) ->
puts_error("Could not open: #{inspect file}. File is not available.")
editor = System.get_env("ELIXIR_EDITOR") || System.get_env("EDITOR") ->
command =
if editor =~ "__FILE__" or editor =~ "__LINE__" do
editor
|> String.replace("__FILE__", inspect(file))
|> String.replace("__LINE__", Integer.to_string(line))
else
"#{editor} #{inspect file}:#{line}"
end
IO.write IEx.color(:eval_info, :os.cmd(String.to_charlist(command)))
true ->
puts_error("Could not open: #{inspect file}. " <>
"Please set the ELIXIR_EDITOR or EDITOR environment variables with the " <>
"command line invocation of your favorite EDITOR.")
end
dont_display_result()
end
def open(invalid) do
puts_error("Invalid arguments for open helper: #{inspect invalid}")
dont_display_result()
end
defp open_mfa(module, fun, arity) do
with {:module, _} <- Code.ensure_loaded(module),
source when is_list(source) <- module.module_info(:compile)[:source] do
open_abstract_code(module, fun, arity, List.to_string(source))
else
_ -> :error
end
end
defp open_abstract_code(module, fun, arity, source) do
fun = Atom.to_string(fun)
with beam when is_list(beam) <- :code.which(module),
{:ok, {_, [abstract_code: {:raw_abstract_v1, code}]}} <- :beam_lib.chunks(beam, [:abstract_code]) do
{_, module_pair, fa_pair} =
Enum.reduce(code, {source, nil, nil}, &open_abstract_code_reduce(&1, &2, fun, arity))
{source, module_pair, fa_pair}
else
_ ->
{source, nil, nil}
end
end
defp open_abstract_code_reduce(entry, {file, module_pair, fa_pair}, fun, arity) do
case entry do
{:attribute, _, :file, {ann_file, _}} ->
if Path.type(ann_file) == :absolute do
{List.to_string(ann_file), module_pair, fa_pair}
else
{file, module_pair, fa_pair}
end
{:attribute, ann, :module, _} ->
{file, {file, :erl_anno.line(ann)}, fa_pair}
{:function, ann, ann_fun, ann_arity, _} ->
case Atom.to_string(ann_fun) do
"MACRO-" <> ^fun when arity == :* or ann_arity == arity + 1 ->
{file, module_pair, fa_pair || {file, :erl_anno.line(ann)}}
^fun when arity == :* or ann_arity == arity->
{file, module_pair, fa_pair || {file, :erl_anno.line(ann)}}
_ ->
{file, module_pair, fa_pair}
end
_ ->
{file, module_pair, fa_pair}
end
end
@doc """
Prints documentation.
"""
def h(module) when is_atom(module) do
case Code.ensure_loaded(module) do
@@ -31,27 +182,7 @@ defmodule IEx.Introspection do
dont_display_result()
end
def h(_) do
puts_error("Invalid arguments for h helper")
dont_display_result()
end
@doc """
Prints the documentation for the given function
with any arity in the list of modules.
"""
def h(modules, function) when is_list(modules) and is_atom(function) do
printed? =
Enum.any?(modules, fn module ->
h_mod_fun(module, function) == :ok
end)
unless printed?, do: no_docs(function)
dont_display_result()
end
def h(module, function) when is_atom(module) and is_atom(function) do
def h({module, function}) when is_atom(module) and is_atom(function) do
case h_mod_fun(module, function) do
:ok ->
:ok
@@ -66,41 +197,7 @@ defmodule IEx.Introspection do
dont_display_result()
end
defp h_mod_fun(mod, fun) when is_atom(mod) do
if docs = Code.get_docs(mod, :docs) do
result = for {{^fun, arity}, _, _, _, _} = doc <- docs, has_content?(doc) do
h(mod, fun, arity)
end
cond do
result != [] ->
:ok
has_callback?(mod, fun) ->
:behaviour_found
true ->
:not_found
end
else
:no_docs
end
end
@doc """
Prints the documentation for the given function
and arity in the list of modules.
"""
def h(modules, function, arity) when is_list(modules) and is_atom(function) and is_integer(arity) do
printed? =
Enum.any?(modules, fn module ->
h_mod_fun_arity(module, function, arity) == :ok
end)
unless printed?, do: no_docs("#{function}/#{arity}")
dont_display_result()
end
def h(module, function, arity) when is_atom(module) and is_atom(function) and is_integer(arity) do
def h({module, function, arity}) when is_atom(module) and is_atom(function) and is_integer(arity) do
case h_mod_fun_arity(module, function, arity) do
:ok ->
:ok
@@ -115,6 +212,31 @@ defmodule IEx.Introspection do
dont_display_result()
end
def h(invalid) do
puts_error("Invalid arguments for h helper: #{inspect invalid}")
dont_display_result()
end
defp h_mod_fun(mod, fun) when is_atom(mod) do
if docs = Code.get_docs(mod, :docs) do
result =
for {{^fun, arity}, _, _, _, _} = doc <- docs, has_content?(doc) do
h({mod, fun, arity})
end
cond do
result != [] ->
:ok
has_callback?(mod, fun) ->
:behaviour_found
true ->
:not_found
end
else
:no_docs
end
end
defp h_mod_fun_arity(mod, fun, arity) when is_atom(mod) do
docs = Code.get_docs(mod, :docs)
@@ -214,7 +336,7 @@ defmodule IEx.Introspection do
end
@doc """
Prints the list of behaviour callbacks for the given module.
Prints the list of behaviour callbacks or a given callback.
"""
def b(mod) when is_atom(mod) do
printer = fn heading, _doc -> puts_info(heading) end
@@ -228,10 +350,7 @@ defmodule IEx.Introspection do
dont_display_result()
end
@doc """
Prints documentation for the given callback function with any arity.
"""
def b(mod, fun) when is_atom(mod) and is_atom(fun) do
def b({mod, fun}) when is_atom(mod) and is_atom(fun) do
filter = &match?({^fun, _}, elem(&1, 0))
case print_callback_docs(mod, filter, &print_doc/2) do
:ok -> :ok
@@ -243,10 +362,7 @@ defmodule IEx.Introspection do
dont_display_result()
end
@doc """
Prints documentation for the given callback function and arity.
"""
def b(mod, fun, arity) when is_atom(mod) and is_atom(fun) and is_integer(arity) do
def b({mod, fun, arity}) when is_atom(mod) and is_atom(fun) and is_integer(arity) do
filter = &match?({^fun, ^arity}, elem(&1, 0))
case print_callback_docs(mod, filter, &print_doc/2) do
:ok -> :ok
@@ -258,6 +374,11 @@ defmodule IEx.Introspection do
dont_display_result()
end
def b(invalid) do
puts_error("Invalid arguments for b helper: #{inspect invalid}")
dont_display_result()
end
defp print_callback_docs(mod, filter, printer) do
case get_callback_docs(mod) do
{callbacks, docs} ->
@@ -273,8 +394,8 @@ defmodule IEx.Introspection do
[] -> :not_found
_ -> :ok
end
other -> other
other ->
other
end
end
@@ -307,24 +428,25 @@ defmodule IEx.Introspection do
do: other
@doc """
Prints the types for the given module.
Prints the types for the given module and type documentation.
"""
def t(module) when is_atom(module) do
_ = case Typespec.beam_types(module) do
nil -> no_beam(module)
[] -> no_types(inspect module)
types -> Enum.each(types, &print_type/1)
case Typespec.beam_types(module) do
nil ->
no_beam(module)
[] ->
no_types(inspect module)
types ->
Enum.each(types, &print_type/1)
end
dont_display_result()
end
@doc """
Prints the given type in module with any arity.
"""
def t(module, type) when is_atom(module) and is_atom(type) do
def t({module, type}) when is_atom(module) and is_atom(type) do
case Typespec.beam_types(module) do
nil -> no_beam(module)
nil ->
no_beam(module)
types ->
printed =
for {_, {^type, _, _args}} = typespec <- types do
@@ -341,12 +463,10 @@ defmodule IEx.Introspection do
dont_display_result()
end
@doc """
Prints the type in module with given arity.
"""
def t(module, type, arity) when is_atom(module) and is_atom(type) and is_integer(arity) do
def t({module, type, arity}) when is_atom(module) and is_atom(type) and is_integer(arity) do
case Typespec.beam_types(module) do
nil -> no_beam(module)
nil ->
no_beam(module)
types ->
printed =
for {_, {^type, _, args}} = typespec <- types, length(args) == arity do
@@ -363,6 +483,11 @@ defmodule IEx.Introspection do
dont_display_result()
end
def t(invalid) do
puts_error("Invalid arguments for t helper: #{inspect invalid}")
dont_display_result()
end
defp print_type_doc(module, type) do
docs = Code.get_docs(module, :type_docs)
{_, _, _, content} = Enum.find(docs, fn({{name, _}, _, _, _}) ->
@@ -372,16 +497,17 @@ defmodule IEx.Introspection do
end
@doc """
Prints the specs for given module.
Prints the specs for given module or function/arity.
"""
def s(module) when is_atom(module) do
case beam_specs(module) do
nil -> no_beam(module)
[] -> no_specs(inspect module)
case Typespec.beam_specs(module) do
nil ->
no_beam(module)
specs ->
printed = for {_kind, {{fun, _arity}, _spec}} = spec <- specs, fun != :__info__ do
print_spec(spec)
end
printed =
for {{fun, _}, _} = spec <- specs, fun != :__info__ do
print_spec(spec)
end
if printed == [] do
no_specs(inspect module)
end
@@ -390,17 +516,14 @@ defmodule IEx.Introspection do
dont_display_result()
end
@doc """
Prints the specs for given module and function.
"""
def s(module, function) when is_atom(module) and is_atom(function) do
case beam_specs(module) do
nil -> no_beam(module)
def s({module, function}) when is_atom(module) and is_atom(function) do
case Typespec.beam_specs(module) do
nil ->
no_beam(module)
specs ->
printed =
for {_kind, {{^function, _arity}, _spec}} = spec <- specs do
for {{^function, _}, _} = spec <- specs do
print_spec(spec)
spec
end
if printed == [] do
@@ -411,17 +534,14 @@ defmodule IEx.Introspection do
dont_display_result()
end
@doc """
Prints the spec in given module, with arity.
"""
def s(module, function, arity) when is_atom(module) and is_atom(function) and is_integer(arity) do
case beam_specs(module) do
nil -> no_beam(module)
def s({module, function, arity}) when is_atom(module) and is_atom(function) and is_integer(arity) do
case Typespec.beam_specs(module) do
nil ->
no_beam(module)
specs ->
printed =
for {_kind, {{^function, ^arity}, _spec}} = spec <- specs do
for {{^function, ^arity}, _} = spec <- specs do
print_spec(spec)
spec
end
if printed == [] do
@@ -432,15 +552,9 @@ defmodule IEx.Introspection do
dont_display_result()
end
defp beam_specs(module) do
specs = beam_specs_tag(Typespec.beam_specs(module), :spec)
callbacks = beam_specs_tag(Typespec.beam_callbacks(module), :callback)
specs && callbacks && Enum.concat(specs, callbacks)
end
defp beam_specs_tag(nil, _), do: nil
defp beam_specs_tag(specs, tag) do
Enum.map(specs, &{tag, &1})
def s(invalid) do
puts_error("Invalid arguments for s helper: #{inspect invalid}")
dont_display_result()
end
defp print_type({:opaque, type}) do
@@ -455,10 +569,10 @@ defmodule IEx.Introspection do
true
end
defp print_spec({kind, {{name, _arity}, specs}}) do
defp print_spec({{name, _arity}, specs}) do
Enum.each specs, fn(spec) ->
binary = Macro.to_string Typespec.spec_to_ast(name, spec)
puts_info("@#{kind} #{binary}")
puts_info("@spec #{binary}")
end
true
end
+393
View File
@@ -0,0 +1,393 @@
defmodule IEx.Pry do
@moduledoc """
The low-level API for prying sessions and setting up breakpoints.
"""
use GenServer
@table __MODULE__
@server __MODULE__
@timeout :infinity
@type id :: integer()
@type break :: {id, module, {function, arity}, pending :: non_neg_integer}
@doc """
Callback for `IEx.pry/1`.
You can invoke this function directly when you are not able to invoke
`IEx.pry/1` as a macro. This function expects the binding (from
`Kernel.binding/0`) and the environment (from `__ENV__/0`).
"""
def pry(binding, %Macro.Env{} = env) do
%{file: file, line: line, module: module, function: function_arity} = env
self = self()
opts = [binding: binding, dot_iex_path: "", env: env, prefix: "pry"]
location =
case function_arity do
{function, arity} ->
"#{Exception.format_mfa(module, function, arity)} (#{Path.relative_to_cwd(file)}:#{line})"
_ ->
"#{Path.relative_to_cwd(file)}:#{line}"
end
whereami =
case whereami(file, line, 2) do
{:ok, lines} -> [?\n, ?\n, lines]
:error -> []
end
# If we are the current evaluator, it is because we just
# reached a pry/breakpoint and the user hit continue().
# In both cases, we are safe to print and the request will
# suceed.
request =
case IEx.Server.evaluator do
{^self, _} ->
IO.puts IEx.color :eval_interrupt, "Break reached: #{location}#{whereami}"
"Prying #{inspect self} at #{location}"
_ ->
"Request to pry #{inspect self} at #{location}#{whereami}"
end
# We cannot use colors because IEx may be off
case IEx.Server.take_over(request, opts) do
:ok ->
:ok
{:error, :no_iex} ->
extra =
if match?({:win32, _}, :os.type) do
" If you are using Windows, you may need to start IEx with the --werl flag."
else
""
end
IO.puts :stdio, "Cannot pry #{inspect self} at #{location}. Is an IEx shell running?" <> extra
{:error, :no_iex}
{:error, _} = error ->
error
end
end
def pry(binding, opts) when is_list(opts) do
vars = for {k, _} when is_atom(k) <- binding, do: {k, nil}
pry(binding, %{:elixir.env_for_eval(opts) | vars: vars})
end
@doc """
Formats the location for `whereami/3` prying.
It receives the `file`, `line` and the snippet `radius` and
returns `{:ok, lines}`, where lines is a list of chardata
containing each formatted line, or `:error`.
The actual line is especially formatted in bold.
"""
def whereami(file, line, radius)
when is_binary(file) and is_integer(line) and is_integer(radius) and radius > 0 do
with true <- File.regular?(file),
[_ | _] = lines <- whereami_lines(file, line, radius) do
{:ok, lines}
else
_ -> :error
end
end
defp whereami_lines(file, line, radius) do
min = max(line - radius - 1, 0)
max = line + radius - 1
file
|> File.stream!
|> Enum.slice(min..max)
|> Enum.with_index(min + 1)
|> Enum.map(&whereami_format_line(&1, line))
end
defp whereami_format_line({line_text, line_number}, line) do
gutter = String.pad_leading(Integer.to_string(line_number), 5, " ")
if line_number == line do
IO.ANSI.format_fragment([:bright, gutter, ": ", line_text, :normal])
else
[gutter, ": ", line_text]
end
end
@doc """
Sets up a breakpoint on the given module/function or
given module/function/arity.
"""
@spec break(module, function, arity, pos_integer) ::
{:ok, id} |
{:error, :recompilation_failed | :no_beam_file | :unknown_function_arity |
:otp_20_is_required | :missing_debug_info | :outdated_debug_info |
:non_elixir_module}
def break(module, function, arity, breaks \\ 1)
when is_atom(module) and is_atom(function) and is_integer(arity) and arity >= 0 and
is_integer(breaks) and breaks > 0 do
GenServer.call(@server, {:break, module, {function, arity}, breaks}, @timeout)
end
@doc """
Resets the breaks on a given breakpoint id.
"""
@spec reset_break(id) :: :ok | :not_found
def reset_break(id) when is_integer(id) do
GenServer.call(@server, {:reset_break, {id, :_, :_, :_}}, @timeout)
end
@doc """
Resets the breaks for the given module, function and arity.
If the module is not instrumented or if the given function
does not have a breakpoint, it is a no-op and it returns
`:not_found`. Otherwise it returns `:ok`.
"""
@spec reset_break(module, function, arity) :: :ok | :not_found
def reset_break(module, function, arity) do
GenServer.call(@server, {:reset_break, {:_, module, {function, arity}, :_}}, @timeout)
end
@doc """
Removes all breakpoints on all modules.
This effectively loads the non-instrumented version of
currently instrumented modules into memory.
"""
@spec remove_breaks :: :ok
def remove_breaks do
GenServer.call(@server, :remove_breaks, @timeout)
end
@doc """
Removes breakpoints in the given module.
This effectively loads the non-instrumented version of
the module into memory.
"""
@spec remove_breaks(module) :: :ok | {:error, :no_beam_file}
def remove_breaks(module) do
GenServer.call(@server, {:remove_breaks, module}, @timeout)
end
@doc """
Returns all breakpoints.
"""
@spec breaks :: [break]
def breaks do
@server
|> GenServer.call(:breaks, @timeout)
|> Enum.sort()
end
## Callbacks
@doc false
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: @server)
end
def init(:ok) do
Process.flag(:trap_exit, true)
:ets.new(@table, [:named_table, :public, write_concurrency: true])
{:ok, 0}
end
def handle_call({:break, module, fa, breaks}, _from, counter) do
# If there is a match for the given module and fa, and
# the module is still instrumented, we just increment
# the breaks counter.
#
# Otherwise we need to invoke the whole instrumentation
# tool chain.
case :ets.match_object(@table, {:_, module, fa, :_}) do
[{ref, module, fa, _}] ->
if instrumented?(module) do
:ets.insert(@table, {ref, module, fa, breaks})
{:reply, {:ok, ref}, counter}
else
:ets.delete(@table, ref)
instrument_and_reply(module, fa, breaks, counter)
end
[] ->
instrument_and_reply(module, fa, breaks, counter)
end
end
def handle_call({:reset_break, pattern}, _from, counter) do
reset =
for {ref, module, fa, _} <- :ets.match_object(@table, pattern) do
if instrumented?(module) do
:ets.insert(@table, {ref, module, fa, 0})
true
else
:ets.delete(@table, ref)
false
end
end
if Enum.any?(reset) do
{:reply, :ok, counter}
else
{:reply, :not_found, counter}
end
end
def handle_call(:breaks, _from, counter) do
entries =
for {id, module, function_arity, breaks} <- :ets.tab2list(@table),
keep_instrumented(id, module) == :ok do
{id, module, function_arity, max(breaks, 0)}
end
{:reply, entries, counter}
end
def handle_call(:remove_breaks, _from, _counter) do
# Make sure to deinstrument before clearing
# up the table to avoid race conditions.
@table
|> :ets.match({:_, :"$1", :_, :_})
|> List.flatten
|> Enum.uniq
|> Enum.each(&deinstrument_if_instrumented/1)
true = :ets.delete_all_objects(@table)
{:reply, :ok, 0}
end
def handle_call({:remove_breaks, module}, _from, counter) do
# Make sure to deinstrumented before clearing
# up the table to avoid race conditions.
reply = deinstrument_if_instrumented(module)
true = :ets.match_delete(@table, {:_, module, :_, :_})
{:reply, reply, counter}
end
defp keep_instrumented(id, module) do
if instrumented?(module) do
:ok
else
:ets.delete(@table, id)
:error
end
end
defp deinstrument_if_instrumented(module) do
if instrumented?(module) do
deinstrument(module)
else
:ok
end
end
defp deinstrument(module) do
with beam when is_list(beam) <- :code.which(module),
{:ok, binary} = File.read(beam) do
:code.purge(module)
{:module, _} = :code.load_binary(module, beam, binary)
:ok
else
_ -> {:error, :no_beam_file}
end
end
defp instrument_and_reply(module, fa, breaks, counter) do
case fetch_elixir_debug_info_with_fa_check(module, fa) do
{:ok, beam, backend, elixir} ->
counter = counter + 1
true = :ets.insert_new(@table, {counter, module, fa, breaks})
entries = :ets.match_object(@table, {:_, module, :_, :_})
{:reply, instrument(beam, backend, elixir, counter, entries), counter}
{:error, _} = error ->
{:reply, error, counter}
end
end
defp fetch_elixir_debug_info_with_fa_check(module, fa) do
case :code.which(module) do
beam when is_list(beam) ->
case :beam_lib.chunks(beam, [:debug_info]) do
{:ok, {_, [debug_info: {:debug_info_v1, backend, {:elixir_v1, map, _} = elixir}]}} ->
case List.keyfind(map.definitions, fa, 0) do
{_, _, _, _} -> {:ok, beam, backend, elixir}
nil -> {:error, :unknown_function_arity}
end
{:ok, {_, [debug_info: {:debug_info_v1, _, _}]}} ->
{:error, :non_elixir_module}
{:error, :beam_lib, {:unknown_chunk, _, _}} ->
{:error, :otp_20_is_required} # TODO: Remove this when we require OTP 20+
{:error, :beam_lib, {:missing_chunk, _, _}} ->
{:error, :missing_debug_info}
_ ->
{:error, :outdated_debug_info}
end
_ ->
{:error, :no_beam_file}
end
end
defp instrument(beam, backend, {:elixir_v1, map, specs}, counter, entries) do
%{attributes: attributes, definitions: definitions, module: module} = map
map = %{map | attributes: [{:iex_pry, true} | attributes],
definitions: Enum.map(definitions, &instrument_definition(&1, map, entries))}
with {:ok, forms} <- backend.debug_info(:erlang_v1, module, {:elixir_v1, map, specs}, []),
{:ok, _, binary, _} <- :compile.noenv_forms(forms, [:return | map.compile_opts]) do
:code.purge(module)
{:module, _} = :code.load_binary(module, beam, binary)
{:ok, counter}
else
_error ->
{:error, :recompilation_failed}
end
end
defp instrument_definition({fa, kind, meta, clauses} = definition, map, entries) do
case List.keyfind(entries, fa, 2) do
{ref, _, ^fa, _} ->
%{module: module, file: file} = map
file =
case meta[:location] do
{file, _} -> file
_ -> file
end
opts = [module: module, file: file, function: fa]
clauses = Enum.map(clauses, &instrument_clause(&1, ref, opts))
{fa, kind, meta, clauses}
nil ->
definition
end
end
defp instrument_clause({meta, args, guards, clause}, ref, opts) do
opts = [line: Keyword.get(meta, :line, 1)] ++ opts
# We store variables on a map ignoring the context.
# In the rare case where variables in different contexts
# have the same name, the last one wins.
{_, binding} =
Macro.prewalk(args, %{}, fn
{name, _, ctx} = var, acc when name != :_ and is_atom(name) and is_atom(ctx) ->
{var, Map.put(acc, name, var)}
expr, acc ->
{expr, acc}
end)
# Generate the take_over condition with the ets lookup.
# Remember this is expanded AST, so no aliases allowed,
# no locals (such as the unary -) and so on.
condition =
quote do
# :ets.update_counter(table, key, {pos, inc, threshold, reset})
case :ets.update_counter(unquote(@table), unquote(ref), {4, unquote(-1), unquote(-1), unquote(-1)}) do
unquote(-1) -> :ok
_ -> :"Elixir.IEx.Pry".pry(unquote(Map.to_list(binding)), unquote(opts))
end
end
{meta, args, guards, {:__block__, [], [condition, clause]}}
end
defp instrumented?(module) do
module.module_info(:attributes)[:iex_pry] == [true]
end
end
+87 -97
View File
@@ -1,5 +1,7 @@
defmodule IEx.State do
@moduledoc false
# This state is exchanged between IEx.Server and
# IEx.Evaluator which is why it is a struct.
defstruct cache: '', counter: 1, prefix: "iex"
@type t :: %__MODULE__{}
end
@@ -45,13 +47,11 @@ defmodule IEx.Server do
@doc """
Returns the PID of the IEx evaluator process if it exists.
"""
@spec evaluator :: pid | nil
@spec evaluator :: {evaluator :: pid, server :: pid} | nil
def evaluator() do
case IEx.Server.local do
nil -> nil
pid ->
{:dictionary, dictionary} = Process.info(pid, :dictionary)
dictionary[:evaluator]
if pid = IEx.Server.local do
{:dictionary, dictionary} = Process.info(pid, :dictionary)
{dictionary[:evaluator], pid}
end
end
@@ -59,31 +59,22 @@ defmodule IEx.Server do
Requests to take over the given shell from the
current process.
"""
@spec take_over(binary, keyword, pos_integer) ::
@spec take_over(binary, keyword) ::
:ok | {:error, :no_iex} | {:error, :refused}
def take_over(identifier, opts, timeout \\ 1000, server \\ whereis()) do
cond do
is_nil(server) ->
{:error, :no_iex}
true ->
ref = make_ref()
send server, {:take?, self(), ref}
def take_over(identifier, opts, server \\ whereis()) do
if is_nil(server) do
{:error, :no_iex}
else
ref = make_ref()
opts = [evaluator: self()] ++ opts
send server, {:take, self(), identifier, ref, opts}
receive do
^ref ->
opts = [evaluator: self()] ++ opts
send server, {:take, self(), identifier, ref, opts}
receive do
{^ref, nil} ->
{:error, :refused}
{^ref, leader} ->
IEx.Evaluator.init(:no_ack, server, leader, opts)
end
after
timeout ->
{:error, :no_iex}
end
receive do
{^ref, nil} ->
{:error, :refused}
{^ref, leader} ->
IEx.Evaluator.init(:no_ack, server, leader, opts)
end
end
end
@@ -110,16 +101,12 @@ defmodule IEx.Server do
defp start_loop(opts, pid, ref) do
receive do
{:take?, other, ref} ->
send(other, ref)
start_loop(opts, pid, ref)
{:take, other, identifier, ref, opts} ->
if allow_take?(identifier) do
send(other, {ref, Process.group_leader})
run(opts)
{:take, take_pid, take_identifier, take_ref, take_opts} ->
if allow_take?(take_identifier) do
send(take_pid, {take_ref, Process.group_leader})
run(take_opts)
else
send(other, {ref, nil})
send(take_pid, {take_ref, nil})
start_loop(opts, pid, ref)
end
@@ -137,7 +124,14 @@ defmodule IEx.Server do
defp run(opts) when is_list(opts) do
IO.puts "Interactive Elixir (#{System.version}) - press Ctrl+C to exit (type h() ENTER for help)"
evaluator = start_evaluator(opts)
loop(run_state(opts), evaluator, Process.monitor(evaluator))
loop(iex_state(opts), evaluator, Process.monitor(evaluator))
end
defp rerun(opts, evaluator, evaluator_ref, input) do
kill_input(input)
IO.puts("")
stop_evaluator(evaluator, evaluator_ref)
run(opts)
end
@doc """
@@ -145,29 +139,16 @@ defmodule IEx.Server do
"""
@spec start_evaluator(keyword) :: pid
def start_evaluator(opts) do
self_pid = self()
self_leader = Process.group_leader
evaluator = opts[:evaluator] ||
:proc_lib.start(IEx.Evaluator, :init, [:ack, self_pid, self_leader, opts])
:proc_lib.start(IEx.Evaluator, :init, [:ack, self(), Process.group_leader, opts])
Process.put(:evaluator, evaluator)
evaluator
end
defp reset_loop(opts, evaluator, evaluator_ref) do
IO.puts("")
# We only kill the evaluator if the new evaluator
# is not the same as the current one. Otherwise
# we end up killing the process that is taking over.
exit_loop(evaluator, evaluator_ref, opts[:evaluator] != evaluator)
run(opts)
end
defp exit_loop(evaluator, evaluator_ref, done? \\ true) do
defp stop_evaluator(evaluator, evaluator_ref) do
Process.delete(:evaluator)
Process.demonitor(evaluator_ref, [:flush])
if done? do
send(evaluator, {:done, self()})
end
send(evaluator, {:done, self()})
:ok
end
@@ -189,11 +170,11 @@ defmodule IEx.Server do
io_error "** (EXIT) interrupted"
loop(%{state | cache: ''}, evaluator, evaluator_ref)
{:input, ^input, :eof} ->
exit_loop(evaluator, evaluator_ref)
stop_evaluator(evaluator, evaluator_ref)
{:input, ^input, {:error, :terminated}} ->
exit_loop(evaluator, evaluator_ref)
stop_evaluator(evaluator, evaluator_ref)
msg ->
handle_take_over(msg, evaluator, evaluator_ref, input, fn ->
handle_take_over(msg, state, evaluator, evaluator_ref, input, fn state ->
wait_input(state, evaluator, evaluator_ref, input)
end)
end
@@ -203,65 +184,75 @@ defmodule IEx.Server do
receive do
{:evaled, ^evaluator, new_state} ->
loop(new_state, evaluator, evaluator_ref)
{:EXIT, _pid, :interrupt} ->
# User did ^G while the evaluator was busy or stuck
io_error "** (EXIT) interrupted"
Process.delete(:evaluator)
Process.exit(evaluator, :kill)
Process.demonitor(evaluator_ref, [:flush])
evaluator = start_evaluator([])
loop(%{state | cache: ''}, evaluator, Process.monitor(evaluator))
msg ->
handle_take_over(msg, evaluator, evaluator_ref, nil,
fn -> wait_eval(state, evaluator, evaluator_ref) end)
handle_take_over(msg, state, evaluator, evaluator_ref, nil,
fn state -> wait_eval(state, evaluator, evaluator_ref) end)
end
end
defp wait_take_over(state, evaluator, evaluator_ref) do
receive do
msg ->
handle_take_over(msg, state, evaluator, evaluator_ref, nil,
fn state -> wait_take_over(state, evaluator, evaluator_ref) end)
end
end
# Take process.
#
# The take? message is received out of band, so we can
# go back to wait for the same input. The take message
# needs to take hold of the IO, so it kills the input,
# re-runs the server OR goes back to the main loop.
#
# A take process may also happen if the evaluator dies,
# then a new evaluator is created to replace the dead one.
defp handle_take_over({:take?, other, ref}, _evaluator, _evaluator_ref, _input, callback) do
send(other, ref)
callback.()
end
defp handle_take_over({:take, other, identifier, ref, opts}, evaluator, evaluator_ref, input, callback) do
kill_input(input)
if allow_take?(identifier) do
send other, {ref, Process.group_leader}
reset_loop(opts, evaluator, evaluator_ref)
else
send other, {ref, nil}
callback.()
defp handle_take_over({:take, other, identifier, ref, opts},
state, evaluator, evaluator_ref, input, callback) do
cond do
evaluator == opts[:evaluator] ->
send other, {ref, Process.group_leader}
kill_input(input)
loop(iex_state(opts), evaluator, evaluator_ref)
allow_take?(identifier) ->
send other, {ref, Process.group_leader}
rerun(opts, evaluator, evaluator_ref, input)
true ->
send other, {ref, nil}
callback.(state)
end
end
defp handle_take_over({:respawn, evaluator}, evaluator, evaluator_ref, input, _callback) do
# User did ^G while the evaluator was busy or stuck
defp handle_take_over({:EXIT, _pid, :interrupt}, state, evaluator, evaluator_ref, input, _callback) do
kill_input(input)
reset_loop([], evaluator, evaluator_ref)
io_error "** (EXIT) interrupted"
Process.delete(:evaluator)
Process.exit(evaluator, :kill)
Process.demonitor(evaluator_ref, [:flush])
evaluator = start_evaluator([])
loop(%{state | cache: ''}, evaluator, Process.monitor(evaluator))
end
defp handle_take_over({:respawn, evaluator}, _state, evaluator, evaluator_ref, input, _callback) do
rerun([], evaluator, evaluator_ref, input)
end
defp handle_take_over({:continue, evaluator}, state, evaluator, evaluator_ref, input, _callback) do
kill_input(input)
send(evaluator, {:done, self()})
wait_take_over(state, evaluator, evaluator_ref)
end
defp handle_take_over({:DOWN, evaluator_ref, :process, evaluator, reason},
evaluator, evaluator_ref, input, _callback) do
_state, evaluator, evaluator_ref, input, _callback) do
try do
io_error Exception.format_banner({:EXIT, evaluator}, reason)
io_error "** (EXIT from #{inspect evaluator}) evaluator process exited with reason: " <>
Exception.format_exit(reason)
catch
type, detail ->
io_error "** (IEx.Error) #{type} when printing EXIT message: #{inspect detail}"
end
kill_input(input)
reset_loop([], evaluator, evaluator_ref)
rerun([], evaluator, evaluator_ref, input)
end
defp handle_take_over(_, _evaluator, _evaluator_ref, _input, callback) do
callback.()
defp handle_take_over(_, state, _evaluator, _evaluator_ref, _input, callback) do
callback.(state)
end
defp kill_input(nil), do: :ok
@@ -278,9 +269,8 @@ defmodule IEx.Server do
## State
defp run_state(opts) do
defp iex_state(opts) do
prefix = Keyword.get(opts, :prefix, "iex")
%IEx.State{prefix: prefix}
end
+2 -2
View File
@@ -11,13 +11,13 @@ defmodule IEx.AutocompleteTest do
defmodule MyServer do
def evaluator do
Process.get(:evaluator)
{Process.get(:evaluator), self()}
end
end
defp eval(line) do
ExUnit.CaptureIO.capture_io(fn ->
evaluator = MyServer.evaluator
{evaluator, _} = MyServer.evaluator
Process.group_leader(evaluator, Process.group_leader)
send evaluator, {:eval, self(), line <> "\n", %IEx.State{}}
assert_receive {:evaled, _, _}
File diff suppressed because it is too large Load Diff
+19 -31
View File
@@ -3,8 +3,6 @@ Code.require_file "../test_helper.exs", __DIR__
defmodule IEx.InteractionTest do
use IEx.Case
## Basic interaction
test "whole output" do
assert capture_io("IO.puts \"Hello world\"", fn ->
IEx.Server.start([dot_iex_path: ""], {IEx, :dont_display_result, []})
@@ -20,6 +18,10 @@ defmodule IEx.InteractionTest do
assert capture_iex("1 + 2") == "3"
end
test "invalid input" do
assert capture_iex("if true do ) false end") =~ "** (SyntaxError) iex:1: \"do\" is missing terminator \"end\". unexpected token: \")\" at line 1"
end
test "multiple vars" do
code = """
<< a, b, c, d, e, f, g, h, i, j :: binary >> = <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10>>
@@ -37,22 +39,6 @@ defmodule IEx.InteractionTest do
assert capture_iex(code) =~ "3"
end
test "exception" do
exception = Regex.escape("** (ArithmeticError) bad argument in arithmetic expression")
assert capture_iex("1 + :atom\n:this_is_still_working")
=~ ~r/^#{exception}.+\n:this_is_still_working$/s
refute capture_iex("1 + :atom\n:this_is_still_working")
=~ ~r/erl_eval/s
end
test "empty history at the start" do
assert capture_iex("v(-1)") =~ "** (RuntimeError) v(-1) is out of bounds"
end
test "empty history at the start redux" do
assert capture_iex("v(1)") =~ "** (RuntimeError) v(1) is out of bounds"
end
test "no break" do
input = """
["a
@@ -72,10 +58,6 @@ defmodule IEx.InteractionTest do
assert capture_iex(input) =~ "** (TokenMissingError) iex:1: incomplete expression"
end
test "invalid input" do
assert capture_iex("if true do ) false end") =~ "** (SyntaxError) iex:1: \"do\" is missing terminator \"end\". unexpected token: \")\" at line 1"
end
test "module definition" do
input = """
defmodule Sample do
@@ -112,19 +94,25 @@ defmodule IEx.InteractionTest do
"<<45, 46, 47>>\n[45, 46, 47]\n%{__struct__: IO.Stream, device: nil, line_or_bytes: :line, raw: true}"
end
test "history size" do
opts = [history_size: 3]
assert capture_iex("1\n2\n3\nv(1)", opts) == "1\n2\n3\n1"
assert "1\n2\n3\n4\n** (RuntimeError) v(1) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(1)", opts)
assert "1\n2\n3\n4\n** (RuntimeError) v(-4) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(-4)", opts)
assert "1\n2\n3\n4\n2\n** (RuntimeError) v(2) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(2)\nv(2)", opts)
test "exception" do
exception = Regex.escape("** (ArithmeticError) bad argument in arithmetic expression")
assert capture_iex("1 + :atom\n:this_is_still_working") =~
~r/^#{exception}.+\n:this_is_still_working$/s
refute capture_iex("1 + :atom\n:this_is_still_working") =~
~r/erl_eval/s
end
test "exception while invoking conflicting helpers" do
import File, only: [open: 1], warn: false
assert capture_iex("open('README.md')", [], [env: __ENV__]) =~
~r"function open/1 imported from both File and IEx.Helpers"
end
test "receive exit" do
assert capture_iex("spawn_link(fn -> exit(:bye) end); Process.sleep(1000)") =~
~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) :bye"
~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) evaluator process exited with reason: :bye"
assert capture_iex("spawn_link(fn -> exit({:bye, [:world]}) end); Process.sleep(1000)") =~
~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) {:bye, \[:world\]}"
~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) evaluator process exited with reason: {:bye, \[:world\]}"
end
test "receive exit from exception" do
@@ -133,7 +121,7 @@ defmodule IEx.InteractionTest do
content = capture_iex("spawn_link(fn -> exit({%ArgumentError{},
[{:not_a_real_module, :function, 0, []}]}) end);
Process.sleep(1000)")
assert content =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) an exception was raised:\n"
assert content =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) evaluator process exited with reason: an exception was raised:\n"
assert content =~ ~r"\s{4}\*\* \(ArgumentError\) argument error\n"
assert content =~ ~r"\s{8}:not_a_real_module\.function/0"
end
+201
View File
@@ -0,0 +1,201 @@
Code.require_file "../test_helper.exs", __DIR__
defmodule IEx.PryTest do
use ExUnit.Case
setup do
on_exit fn -> IEx.Pry.remove_breaks() end
end
describe "whereami" do
test "shows lines with radius" do
Application.put_env(:elixir, :ansi_enabled, true)
{:ok, contents} = IEx.Pry.whereami(__ENV__.file, 3, 2)
assert IO.iodata_to_binary(contents) == """
1: Code.require_file "../test_helper.exs", __DIR__
2:\s
\e[1m 3: defmodule IEx.PryTest do
\e[22m 4: use ExUnit.Case
5:\s
"""
{:ok, contents} = IEx.Pry.whereami(__ENV__.file, 1, 4)
assert IO.iodata_to_binary(contents) == """
\e[1m 1: Code.require_file "../test_helper.exs", __DIR__
\e[22m 2:\s
3: defmodule IEx.PryTest do
4: use ExUnit.Case
5:\s
"""
after
Application.delete_env(:elixir, :ansi_enabled)
end
test "returns error for unknown files" do
assert IEx.Pry.whereami("unknown", 3, 2) == :error
end
test "returns error for out of range lines" do
assert IEx.Pry.whereami(__ENV__.file, 1000, 2) == :error
end
end
if :erlang.system_info(:otp_release) >= '20' do
describe "break" do
test "sets up a breakpoint on the given module" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert instrumented?(URI)
assert [_] = IEx.Pry.breaks()
end
test "sets up multiple breakpoints in the same module" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert instrumented?(URI)
assert IEx.Pry.break(URI, :parse, 1) == {:ok, 2}
assert instrumented?(URI)
assert [_, _] = IEx.Pry.breaks()
end
test "reinstruments if module has been reloaded" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert instrumented?(URI)
deinstrument!(URI)
refute instrumented?(URI)
assert IEx.Pry.break(URI, :parse, 1) == {:ok, 2}
assert instrumented?(URI)
assert [_, _] = IEx.Pry.breaks()
end
test "returns id when breakpoint is already set" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert [_] = IEx.Pry.breaks()
end
test "builds new id when breakpoint is already set on deinstrument" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
deinstrument!(URI)
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 2}
assert [_] = IEx.Pry.breaks()
end
test "errors when setting up a break with no beam" do
assert IEx.Pry.break(__MODULE__, :setup, 2) == {:error, :no_beam_file}
end
test "errors when setting up a break for unknown function" do
assert IEx.Pry.break(URI, :unknown, 2) == {:error, :unknown_function_arity}
end
test "errors for non elixir modules" do
assert IEx.Pry.break(:elixir, :unknown, 2) == {:error, :non_elixir_module}
end
end
describe "breaks" do
test "returns all breaks" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert IEx.Pry.breaks() ==
[{1, URI, {:decode_query, 2}, 1}]
assert IEx.Pry.break(URI, :decode_query, 2, 10) == {:ok, 1}
assert IEx.Pry.breaks() ==
[{1, URI, {:decode_query, 2}, 10}]
assert IEx.Pry.break(URI, :parse, 1, 1) == {:ok, 2}
assert IEx.Pry.breaks() ==
[{1, URI, {:decode_query, 2}, 10}, {2, URI, {:parse, 1}, 1}]
end
test "sets negative break to 0" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
:ets.insert(IEx.Pry, {1, URI, {:decode_query, 2}, -1})
assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}]
end
test "do not return break points for deinstrumented modules" do
assert IEx.Pry.break(URI, :parse, 1) == {:ok, 1}
assert IEx.Pry.breaks() == [{1, URI, {:parse, 1}, 1}]
deinstrument!(URI)
assert IEx.Pry.breaks() == []
end
end
describe "reset_break" do
test "resets break for given id" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert IEx.Pry.reset_break(1) == :ok
assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}]
end
test "resets break for given mfa" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert IEx.Pry.reset_break(URI, :decode_query, 2) == :ok
assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}]
end
test "returns not_found if module is deinstrumented" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
deinstrument!(URI)
assert IEx.Pry.reset_break(URI, :decode_query, 2) == :not_found
assert IEx.Pry.breaks() == []
end
test "returns not_found if mfa has no break" do
assert IEx.Pry.reset_break(URI, :decode_query, 2) == :not_found
end
test "returns not_found if id is deinstrumented" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
deinstrument!(URI)
assert IEx.Pry.reset_break(1) == :not_found
assert IEx.Pry.breaks() == []
end
test "returns not_found if id has no break" do
assert IEx.Pry.reset_break(1) == :not_found
end
end
describe "remove_breaks" do
test "removes all breaks" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert IEx.Pry.remove_breaks() == :ok
assert IEx.Pry.breaks() == []
end
test "removes all breaks even if module is deinstrumented" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
deinstrument!(URI)
assert IEx.Pry.remove_breaks() == :ok
assert IEx.Pry.breaks() == []
end
test "remove breaks in a given module" do
assert IEx.Pry.remove_breaks(Date.Range) == :ok
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
assert IEx.Pry.break(Date.Range, :__struct__, 1) == {:ok, 2}
assert IEx.Pry.remove_breaks(Date.Range) == :ok
assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}]
end
test "remove breaks in a given module even if deinstrumented" do
assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1}
deinstrument!(URI)
assert IEx.Pry.breaks() == []
end
end
end
defp instrumented?(module) do
module.module_info(:attributes)[:iex_pry] == [true]
end
defp deinstrument!(module) do
beam = :code.which(module)
:code.purge(module)
{:module, _} = :code.load_binary(module, beam, File.read!(beam))
:ok
end
end
+38 -28
View File
@@ -3,40 +3,50 @@ Code.require_file "../test_helper.exs", __DIR__
defmodule IEx.ServerTest do
use IEx.Case
# Options
describe "options" do
test "prefix" do
assert capture_io(fn ->
boot([prefix: "pry"])
end) =~ "pry(1)> "
end
test "prefix option" do
assert capture_io(fn ->
boot([prefix: "pry"])
end) =~ "pry(1)> "
test "env" do
assert capture_io("__ENV__.file", fn ->
boot([env: __ENV__])
end) =~ "server_test.exs"
end
end
test "env option" do
assert capture_io("__ENV__.file", fn ->
boot([env: __ENV__])
end) =~ "server_test.exs"
end
describe "take_over" do
test "allows take over of the shell during boot" do
assert capture_io("Y\na+b", fn ->
server = self()
boot([], fn ->
opts = [prefix: "dbg", binding: [a: 1, b: 2]]
IEx.Server.take_over("iex:13", opts, server)
end)
end) =~ "dbg(1)> "
end
# Take over
test "continues if take over is refused" do
assert capture_io("N\n", fn ->
server = self()
boot([], fn ->
opts = [prefix: "dbg", binding: [a: 1, b: 2]]
IEx.Server.take_over("iex:13", opts, server)
end)
end) =~ "iex(1)> "
end
test "allows take over of the shell during boot" do
assert capture_io("Y\na+b", fn ->
server = self()
boot([], fn ->
opts = [prefix: "dbg", binding: [a: 1, b: 2]]
IEx.Server.take_over("iex:13", opts, 1000, server)
end)
end) =~ "dbg(1)> "
end
test "fails if callback during boot fails" do
assert capture_io(fn ->
boot([], fn -> exit(0) end)
end) == ""
end
test "does not operate if callback during boot fails" do
assert capture_io(fn ->
boot([], fn -> exit(0) end)
end) == ""
end
test "take over fails when there is no shell" do
assert IEx.Server.take_over("iex:13", [], 10) == {:error, :no_iex}
test "fails when there is no shell" do
assert IEx.Server.take_over("iex:13", []) == {:error, :no_iex}
end
end
test "pry wraps around take over" do
+2
View File
@@ -1,3 +1,5 @@
System.put_env("ELIXIR_EDITOR", "echo")
:ok = Application.start(:iex)
IEx.configure([colors: [enabled: false]])
ExUnit.start [trace: "--trace" in System.argv, assert_receive_timeout: 500]
+12
View File
@@ -201,6 +201,15 @@ defmodule Mix do
@doc """
Returns the Mix environment.
This function should not be used at runtime in application code (as opposed
to infrastructure and build code like Mix tasks). Mix is a build tool and may
not be available after the code is compiled (for example in a release).
To differentiate the program behavior depending on the environment, it is
recommended to use application environment through `Application.get_env/3`.
Proper configuration can be set in `Mix.Config` files, often per-environment
(see `Mix.Config.import_config/1` for more information).
"""
def env do
# env is not available on bootstrapping, so set a :dev default
@@ -212,6 +221,9 @@ defmodule Mix do
Be careful when invoking this function as any project
configuration won't be reloaded.
This function should not be used at runtime in application code
(see `env/0` for more information).
"""
def env(env) when is_atom(env) do
Mix.State.put(:env, env)
+3 -3
View File
@@ -51,9 +51,9 @@ defmodule Mix.Compilers.Erlang do
compile(manifest, files, src_ext, opts, callback)
end
def compile(manifest, mappings, src_ext, dest_ext, force, callback) when is_boolean(force) do
IO.warn "Mix.Compilers.Erlang.compile/6 with a boolean as 5th argument is deprecated, " <>
"please pass [force: true]"
def compile(manifest, mappings, src_ext, dest_ext, force, callback) when is_boolean(force) or is_nil(force) do
IO.warn "Mix.Compilers.Erlang.compile/6 with a boolean or nil as 5th argument is deprecated, " <>
"please pass [force: true] or [] instead"
compile(manifest, mappings, src_ext, dest_ext, [force: force], callback)
end
+2
View File
@@ -147,6 +147,8 @@ defmodule Mix.Tasks.Compile.Erlang do
end
{:attribute, _, :behaviour, behaviour} ->
%{erl | behaviours: [behaviour | erl.behaviours]}
{:attribute, _, :compile, value} when is_list(value) ->
%{erl | compile: value ++ erl.compile}
{:attribute, _, :compile, value} ->
%{erl | compile: [value | erl.compile]}
_ ->
+13 -7
View File
@@ -193,14 +193,20 @@ defmodule Mix.Tasks.Compile.Protocols do
Map.put(protocols, protocol, true)
end)
removed_metadata =
old_metadata -- new_metadata
removed_protocols =
for {protocol, :protocol, _beam} <- removed_metadata,
remove_consolidated(protocol, output),
do: {protocol, true},
into: %{}
protocols =
Enum.reduce(old_metadata -- new_metadata, protocols, fn
{_, {:impl, protocol}, _beam}, protocols ->
Map.put(protocols, protocol, true)
{protocol, :protocol, _beam}, protocols ->
remove_consolidated(protocol, output)
protocols
end)
for {_, {:impl, protocol}, _beam} <- removed_metadata,
not Map.has_key?(removed_protocols, protocol),
do: {protocol, true},
into: protocols
Map.keys(protocols)
end
+11 -2
View File
@@ -13,7 +13,16 @@ defmodule Mix.Tasks.Escript.Build do
be installed, as Elixir is embedded as part of the escript.
This task guarantees the project and its dependencies are
compiled and packages them inside an escript.
compiled and packages them inside an escript. Before invoking
`mix escript.build`, it is only necessary to define a `:escript`
key with a `:main_module` option in your `mix.exs` file:
escript: [main_module: MyApp.CLI]
By default, this task removes documentation and debugging
chunks from the compiled `.beam` files to reduce the size of
the escript. If this is not desired, check the `:strip_beams`
option.
> Note: escripts do not support projects and dependencies
> that need to store or read artifacts from the priv directory.
@@ -261,7 +270,7 @@ defmodule Mix.Tasks.Escript.Build do
defp strip_beam(beam) when is_binary(beam) do
{:ok, _, all_chunks} = :beam_lib.all_chunks(beam)
filtered_chunks = ['CInf', 'Abst', 'Dbgi']
filtered_chunks = ['Abst', 'CInf', 'Dbgi', 'ExDc']
significant_chunks =
for {name, _} = pair <- all_chunks, name not in filtered_chunks, do: pair
{:ok, built_module} = :beam_lib.build_module(significant_chunks)
+1 -3
View File
@@ -83,9 +83,7 @@ defmodule Mix.Tasks.Run do
# Start app after rewriting System.argv,
# but before requiring and evaling.
if opts[:start] != false do
Mix.Task.run "app.start", args
end
Mix.Task.run "app.start", args
process_load(opts, expr_evaluator)
@@ -42,6 +42,41 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do
end
end
test "compiles after converting a protocol into a standard module", context do
Mix.Project.push MixTest.Case.Sample
in_tmp context.test, fn ->
File.mkdir_p!("lib")
assert Mix.Task.run("compile")
# Define a local protocol
File.write!("lib/protocol.ex", """
defprotocol Compile.Protocol do
def foo(a)
end
defimpl Compile.Protocol, for: Integer do
def foo(a), do: a
end
""")
assert compile_elixir_and_protocols() == :ok
mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
File.rm!("lib/protocol.ex")
# Define a standard module
File.write!("lib/protocol.ex", """
defmodule Compile.Protocol do
end
""")
assert compile_elixir_and_protocols() == :noop
# Delete a local protocol
File.rm!("lib/protocol.ex")
assert compile_elixir_and_protocols() == :noop
refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
end
end
test "compiles and consolidates deps protocols", context do
Mix.Project.push MixTest.Case.Sample
+13
View File
@@ -37,6 +37,19 @@ defmodule Mix.Tasks.RunTest do
purge [GitRepo]
end
test "does not start applications on --no-start", context do
in_tmp context.test, fn ->
Mix.Tasks.Run.run ["--no-start", "-e", "send self(), {:apps, Application.started_applications}"]
assert_received {:apps, apps}
refute List.keyfind(apps, :sample, 0)
Mix.Task.clear
Mix.Tasks.Run.run ["-e", "send self(), {:apps, Application.started_applications}"]
assert_received {:apps, apps}
assert List.keyfind(apps, :sample, 0)
end
end
test "run errors on missing files", context do
in_tmp context.test, fn ->
assert_raise Mix.Error, "No files matched pattern \"non-existent\" given to --require", fn ->
+1 -1
View File
@@ -1,6 +1,6 @@
{application, elixir,
[{description, "elixir"},
{vsn, "1.5.0-dev"},
{vsn, "1.5.0"},
{modules, [
elixir
]},