Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c345dab9c1 | ||
|
|
2d46eda619 | ||
|
|
beb9b1b3ef | ||
|
|
cb512a7f17 | ||
|
|
e4fdeb6b79 | ||
|
|
6b22171018 | ||
|
|
f0112289e6 | ||
|
|
72e18a0a0c | ||
|
|
e2be3f652e | ||
|
|
490bd32322 | ||
|
|
1bfcba133a | ||
|
|
c3d024c16c | ||
|
|
1530a761d5 | ||
|
|
5197b6ece6 |
@@ -1 +0,0 @@
|
||||
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
||||
+10
-14
@@ -1,15 +1,11 @@
|
||||
/.eunit
|
||||
/.release
|
||||
/docs
|
||||
/ebin
|
||||
/lib/*/ebin/*
|
||||
/lib/*/tmp
|
||||
/lib/*/test/tmp
|
||||
/lib/elixir/src/elixir.app.src
|
||||
/lib/elixir/src/*_lexer.erl
|
||||
/lib/elixir/src/*_parser.erl
|
||||
/lib/elixir/test/ebin
|
||||
/rel/elixir
|
||||
.formatter.exs
|
||||
/_build/
|
||||
/cover/
|
||||
/deps/
|
||||
/doc/
|
||||
/.fetch
|
||||
erl_crash.dump
|
||||
.dialyzer_plt
|
||||
.dialyzer.base_plt
|
||||
*.ez
|
||||
n8n_openai_adapter-*.tar
|
||||
/tmp/
|
||||
/result
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
language: erlang
|
||||
script: "make compile && rm -rf .git && make test"
|
||||
notifications:
|
||||
irc: "irc.freenode.org#elixir-lang"
|
||||
recipients:
|
||||
- jose.valim@plataformatec.com.br
|
||||
- eric.meadows.jonsson@gmail.com
|
||||
otp_release:
|
||||
- 17.0
|
||||
-963
@@ -1,963 +0,0 @@
|
||||
# v0.13.2 (2014-05-11)
|
||||
|
||||
* Enhancements
|
||||
* [Application] Add an Application module with common functions to work with OTP applications
|
||||
* [Exception] Add `Exception.message/1`, `Exception.format_banner/1`, `Exception.format_exit/1` and `Exception.format/1`
|
||||
* [File] Add `File.ln_s/1`
|
||||
* [Mix] `mix deps.clean` now works accross environments
|
||||
* [Mix] Support line numbers in `mix test`, e.g. test/some/file_test.exs:12
|
||||
* [Mix] Use `@file` attributes to detect dependencies in between `.ex` and external files. This means changing an `.eex` file will no longer recompile the whole project only the files that depend directly on it
|
||||
* [Mix] Support application configurations in `config/config.exs` which can be customized by specifying your own `:config_path`
|
||||
* [Mix] Support user-wide configuration with `~/.mix/config.exs`
|
||||
* [Mix] `mix help` now uses ANSI formatting to print guides
|
||||
* [Regex] Support functions in `Regex.replace/4`
|
||||
* [String] Support `:parts` in `String.split/3`
|
||||
|
||||
* Bug fixes
|
||||
* [Code] Ensure we don't lose the caller stacktrace on code evaluation
|
||||
* [IEx] Exit signals now exits the IEx evaluator and a new one is spawned on its place
|
||||
* [IEx] Ensure we don't prune too much stacktrace when reporting failures
|
||||
* [IEx] Fix an issue where `iex.bat` on Windows was not passing the proper parameters forward
|
||||
* [Kernel] Ensure modules defined on root respect defined aliases
|
||||
* [Kernel] Do not wrap single lists in `:__block__`
|
||||
* [Kernel] Ensure emitted beam code works nicely with dialyzer
|
||||
* [Kernel] Do not allow a module named `Elixir` to be defined
|
||||
* [Kernel] Create remote funs even if mod is a variable in capture `&mod.fun/arity`
|
||||
* [Kernel] Improve compiler message when duplicated modules are detected
|
||||
* [Mix] Generate `.gitignore` for `--umbrella` projects
|
||||
* [Mix] Verify if a git dependency in deps has a proper git checkout and clean it automatically when it doesn't
|
||||
* [Mix] Ensure `mix test` works with `IEx.pry/0`
|
||||
* [System] Convert remaining functions in System to rely on char data
|
||||
|
||||
* Soft deprecations (no warnings emitted)
|
||||
* [Application] `use Application.Behaviour` is deprecated in favor of `use Application`
|
||||
* [Exception] `exception.message` is deprecated in favor `Exception.message/1` for retrieving exception messages
|
||||
* [Kernel] `is_exception/1`, `is_record/1` and `is_record/2` are deprecated in favor of `Exception.exception?1`, `Record.record?/1` and `Record.record?/2`
|
||||
* [Mix] `Mix.project/0` is deprecated in favor of `Mix.Project.config/0`
|
||||
* [Process] `Process.spawn/1`, `Process.spawn/3`, `Process.spawn_link/1`, `Process.spawn_link/3`, `Process.spawn_monitor/1`, `Process.spawn_monitor/3`, `Process.send/2` and `Process.self/0` are deprecated in favor of the ones in `Kernel`
|
||||
|
||||
* Deprecations
|
||||
* [IEx] IEx.Options is deprecated in favor of `IEx.configure/1` and `IEx.configuration/0`
|
||||
* [Kernel] `lc` and `bc` comprehensions are deprecated in favor of `for`
|
||||
* [Macro] `Macro.safe_terms/1` is deprecated
|
||||
* [Process] `Process.delete/0` is deprecated
|
||||
* [Regex] Deprecate `:global` option in `Regex.split/3` in favor of `parts: :infinity`
|
||||
* [String] Deprecate `:global` option in `String.split/3` in favor of `parts: :infinity`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [ExUnit] `ExUnit.Test` and `ExUnit.TestCase` has been converted to structs
|
||||
* [ExUnit] The test and callback context has been converted to maps
|
||||
* [Kernel] `File.Stat`, `HashDict`, `HashSet`, `Inspect.Opts`, `Macro.Env`, `Range`, `Regex` and `Version.Requirement` have been converted to structs. This means `is_record/2` checks will no longer work, instead, you can pattern match on them using `%Range{}` and similar
|
||||
* [URI] The `URI.Info` record has now become the `URI` struct
|
||||
* [Version] The `Version.Schema` record has now become the `Version` struct
|
||||
|
||||
# v0.13.1 (2014-04-27)
|
||||
|
||||
* Enhancements
|
||||
* [Mix] Support `MIX_EXS` as configuration for running the current mix.exs file
|
||||
* [Mix] Support Hex out of the box. This means users do not need to install Hex directly, instead, Mix will prompt whenever there is a need to have Hex installed
|
||||
|
||||
* Bug fixes
|
||||
* [ExUnit] Ensure doctest failures are properly reported
|
||||
* [Kernel] Fix a bug where comprehensions arguments were not properly take into account in the variable scope
|
||||
* [Mix] Fix issue on rebar install when the endpoint was redirecting to a relative uri
|
||||
|
||||
* Soft deprecations (no warnings emitted)
|
||||
* [Kernel] `iolist_size` and `iolist_to_binary` are deprecated in favor of `iodata_length` and `iodata_to_binary`
|
||||
* [String] `String.to_char_list/1` is deprecated in favor of `List.from_char_data/1`
|
||||
* [String] `String.from_char_list/1` is deprecated in favor of `String.from_char_data/1`
|
||||
|
||||
* Deprecations
|
||||
* [Mix] `:env` key in project configuration is deprecated
|
||||
* [Regex] `Regex.groups/1` is deprecated in favor of `Regex.names/1`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Macro] `Macro.unpipe/1` now returns tuples and `Macro.pipe/2` was removed in favor of `Macro.pipe/3` which explicitly expects the second element of the tuple returned by the new `Macro.unpipe/1`
|
||||
* [Path] The functions in Path now only emit strings as result, regardless if the input was a char list or a string
|
||||
* [Path] Atoms are no longer supported in Path functions
|
||||
* [Regex] Regexes are no longer unicode by default. Instead, they must be explicitly marked with the `u` option
|
||||
|
||||
# v0.13.0 (2014-04-20)
|
||||
|
||||
* Enhancements
|
||||
* [Base] Add `Base` module which does conversions to bases 16, 32, hex32, 64 and url64
|
||||
* [Code] Add `Code.eval_file/2`
|
||||
* [Collectable] Add the `Collectable` protocol that empowers `Enum.into/2` and `Stream.into/2` and the `:into` option in comprehensions
|
||||
* [Collectable] Implement `Collectable` for lists, dicts, bitstrings, functions and provide both `File.Stream` and `IO.Stream`
|
||||
* [EEx] Add `handle_body/1` callback to `EEx.Engine`
|
||||
* [Enum] Add `Enum.group_by/2`, `Enum.into/2`, `Enum.into/3`, `Enum.traverse/2` and `Enum.sum/2`
|
||||
* [ExUnit] Randomize cases and tests suite runs, allow seed configuration and the `--seed` flag via `mix test`
|
||||
* [ExUnit] Support `--only` for filtering when running tests with `mix test`
|
||||
* [ExUnit] Raise an error if another `capture_io` process already captured the device
|
||||
* [ExUnit] Improve formatter to show source code and rely on lhs and rhs (instead of expected and actual)
|
||||
* [IEx] Allow prompt configuration with the `:prompt` option
|
||||
* [IEx] Use werl on Windows
|
||||
* [Kernel] Support `ERL_PATH` in `bin/elixir`
|
||||
* [Kernel] Support interpolation in keyword syntax
|
||||
* [Map] Add a Map module and support 17.0 maps and structs
|
||||
* [Mix] Add dependency option `:only` to specify the dependency environment. `mix deps.get` and `mix deps.update` works accross all environment unless `--only` is specified
|
||||
* [Mix] Add `Mix.Shell.prompt/1`
|
||||
* [Mix] Ensure the project is compiled in case Mix' CLI cannot find a task
|
||||
* [Node] Add `Node.ping/1`
|
||||
* [Process] Include `Process.send/3` and support the `--gen-debug` option
|
||||
* [Regex] Regexes no longer need the "g" option when there is a need to use named captures
|
||||
* [Stream] Add `Stream.into/2` and `Stream.into/3`
|
||||
* [StringIO] Add a `StringIO` module that allows a String to be used as IO device
|
||||
* [System] Add `System.delete_env/1` to remove a variable from the environment
|
||||
|
||||
* Bug fixes
|
||||
* [CLI] Ensure `--app` is handled as an atom before processing
|
||||
* [ExUnit] Ensure `ExUnit.Assertions` does not emit compiler warnings for `assert_receive`
|
||||
* [Kernel] Ensure the same pid is not queued twice in the parallel compiler
|
||||
* [Macro] `Macro.to_string/2` considers proper precedence when translating `!(foo > bar)` into a string
|
||||
* [Mix] Automatically recompile on outdated Elixir version and show proper error messages
|
||||
* [Mix] Ensure generated `.app` file includes core dependencies
|
||||
* [Mix] Allow a dependency with no SCM to be overridden
|
||||
* [Mix] Allow queries in `mix local.install` URL
|
||||
* [OptionParser] Do not recognize undefined aliases as switches
|
||||
|
||||
* Soft deprecations (no warnings emitted)
|
||||
* [Kernel] `lc` and `bc` comprehensions are deprecated in favor of `for`
|
||||
* [ListDict] `ListDict` is deprecated in favor of `Map`
|
||||
* [Record] `defrecord/2`, `defrecordp/3`, `is_record/1` and `is_record/2` macros in Kernel are deprecated. Instead, use the new macros and API defined in the `Record` module
|
||||
|
||||
* Deprecations
|
||||
* [Dict] `Dict.empty/1`, `Dict.new/1` and `Dict.new/2` are deprecated
|
||||
* [Exception] `Exception.normalize/1` is deprecated in favor of `Exception.normalize/2`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [ExUnit] Formatters are now required to be a GenEvent and `ExUnit.run/2` returns a map with results
|
||||
|
||||
# v0.12.5 (2014-03-09)
|
||||
|
||||
* Bug fixes
|
||||
* [Kernel] Ensure `try` does not generate an after clause. Generating an after clause forbade clauses in the `else` part from being tail recursive. This should improve performance and memory consumption of `Stream` functions
|
||||
* [Mix] Automatically recompile on outdated Elixir version and show proper error messages
|
||||
|
||||
* Deprecations
|
||||
* [File] `File.stream_to!/3` is deprecated
|
||||
* [GenFSM] `GenFSM` is deprecated
|
||||
* [Kernel] `%` for sigils is deprecated in favor of `~`
|
||||
* [Kernel] `is_range/1` and `is_regex/1` are deprecated in favor of `Range.range?/1` and `Regex.regex?/1`
|
||||
* [Stream] `Stream.after/1` is deprecated
|
||||
* [URI] `URI.decode_query/1` is deprecated in favor of `URI.decode_query/2` with explicit dict argument
|
||||
* [URI] Passing lists as key or values in `URI.encode_query/1` is deprecated
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Mix] Remove `MIX_GIT_FORCE_HTTPS` as Git itself already provides mechanisms for doing so
|
||||
|
||||
# v0.12.4 (2014-02-12)
|
||||
|
||||
* Enhancements
|
||||
* [Mix] `mix deps.get` and `mix deps.update` no longer compile dependencies afterwards. Instead, they mark the dependencies which are going to be automatically compiled next time `deps.check` is invoked (which is done automatically by most mix tasks). This means users should have a better workflow when migrating in between environments
|
||||
|
||||
* Deprecations
|
||||
* [Kernel] `//` for default arguments is deprecated in favor of `\\`
|
||||
* [Kernel] Using `%` for sigils is deprecated in favor of `~`. This is a soft deprecation, no warnings will be emitted for it in this release
|
||||
* [Kernel] Using `^` inside function clause heads is deprecated, please use a guard instead
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [ExUnit] `CaptureIO` returns an empty string instead of nil when there is no capture
|
||||
* [Version] The `Version` module now only works with SemVer. The functions `Version.parse/1` and `Version.parse_requirement/1` now return `{:ok,res} | :error` for the cases you want to handle non SemVer cases manually. All other functions will trigger errors on non semantics versions
|
||||
|
||||
# v0.12.3 (2014-02-02)
|
||||
|
||||
* Enhancements
|
||||
* [Kernel] Warnings now are explicitly tagged with "warning:" in messages
|
||||
* [Kernel] Explicit functions inlined by the compiler, including operators. This means that `Kernel.+/2` will now expand to `:erlang.+/2` and so on
|
||||
* [Mix] Do not fail if a Mix dependency relies on an outdated Elixir version
|
||||
* [Process] Add `Process.send/2` and `Process.send_after/3`
|
||||
* [Version] Add `Version.compare/2`
|
||||
|
||||
* Bug fixes
|
||||
* [Atom] Inspect `:...` and `:foo@bar` without quoting
|
||||
* [Keyword] The list `[1, 2, three: :four]` now correctly expands to `[1, 2, {:three, :four}]`
|
||||
* [Kernel] Ensure undefined `@attributes` shows proper stacktrace in warnings
|
||||
* [Kernel] Guarantee nullary funs/macros are allowed in guards
|
||||
* [Process] Ensure monitoring functions are inlined by the compiler
|
||||
|
||||
* Deprecations
|
||||
* [IEx] The helper `m/0` has been deprecated. The goal is to group all runtime statistic related helpers into a single module
|
||||
* [Kernel] `binary_to_term/1`, `binary_to_term/2`, `term_to_binary/1` and `term_to_binary/2` are deprecated in favor of their counterparts in the `:erlang` module
|
||||
* [Kernel] `//` for default arguments is deprecated in favor of `\\`. This is a soft deprecation, no warnings will be emitted for it in this release
|
||||
* [Kernel] Deprecated `@behavior` in favor of `@behaviour`
|
||||
* [Record] `to_keywords`, `getter` and `list getter` functionalities in `defrecordp` are deprecated
|
||||
* [Record] `Record.import/2` is deprecated
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Dict] Implementations of `equal?/2` and `merge/2` in `HashDict` and `ListDict` are no longer polymorphic. To get polymorphism, use the functions in `Dict` instead
|
||||
* [File] `File.cp/3` and `File.cp_r/3` no longer carry Unix semantics where the function behaves differently if the destination is an existing previous directory or not. It now always copies source to destination, doing it recursively in the latter
|
||||
* [IEx] IEx now loads the `.iex.exs` file instead of `.iex`
|
||||
* [Kernel] Remove `**` from the list of allowed operators
|
||||
* [Kernel] Limit sigils delimiters to one of the following: `<>`, `{}`, `[]`, `()`, `||`, `//`, `"` and `'`
|
||||
* [Range] `Range` is no longer a record, instead use `first .. last` if you need pattern matching
|
||||
* [Set] Implementations of `difference/2`, `disjoint?/2`, `equal?/2`, `intersection/2`, `subset?/2` and `union/2` in `HashSet` are no longer polymorphic. To get polymorphism, use the functions in `Set` instead
|
||||
|
||||
# v0.12.2 (2014-01-15)
|
||||
|
||||
* Enhancements
|
||||
* [EEx] Allow `EEx.AssignsEngine` to accept any Dict
|
||||
* [Enum] Add `Enum.flat_map_reduce/3`
|
||||
* [ExUnit] Support `@moduletag` in ExUnit cases
|
||||
* [Kernel] Improve stacktraces to be relative to the compilation path and include the related application
|
||||
* [Stream] Add `Stream.transform/3`
|
||||
|
||||
* Bug fixes
|
||||
* [ExUnit] `:include` in ExUnit only has effect if a test was previously excluded with `:exclude`
|
||||
* [ExUnit] Only run `setup_all` and `teardown_all` if there are tests in the case
|
||||
* [Kernel] Ensure bitstring modifier arguments are expanded
|
||||
* [Kernel] Ensure compiler does not block on missing modules
|
||||
* [Kernel] Ensure `<>/2` works only with binaries
|
||||
* [Kernel] Fix usage of string literals inside `<<>>` when `utf8`/`utf16`/`utf32` is used as specifier
|
||||
* [Mix] Ensure mix properly copies _build dependencies on Windows
|
||||
|
||||
* Deprecations
|
||||
* [Enum] Deprecate `Enum.first/1` in favor of `Enum.at/2` and `List.first/1`
|
||||
* [Kernel] Deprecate continuable heredocs. In previous versions, Elixir would continue parsing on the same line the heredoc started, this behaviour has been deprecated
|
||||
* [Kernel] `is_alive/0` is deprecated in favor of `Node.alive?`
|
||||
* [Kernel] `Kernel.inspect/2` with `Inspect.Opts[]` is deprecated in favor of `Inspect.Algebra.to_doc/2`
|
||||
* [Kernel] `Kernel.inspect/2` with `:raw` option is deprecated, use `:records` option instead
|
||||
* [Kernel] Deprecate `<-/2` in favor of `send/2`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [String] Change `String.next_grapheme/1` and `String.next_codepoint/1` to return `nil` on string end
|
||||
|
||||
# v0.12.1 (2014-01-04)
|
||||
|
||||
* Enhancements
|
||||
* [ExUnit] Support `:include` and `:exclude` configuration options to filter which tests should run based on their tags. Those options are also supported via `mix test` as `--include` and `--exclude`
|
||||
* [ExUnit] Allow doctests to match against `#MyModule<>`
|
||||
|
||||
* Bug fixes
|
||||
* [CLI] Abort when a pattern given to elixirc does not match any file
|
||||
* [Float] Fix `Float.parse/1` to handle numbers of the form "-0.x"
|
||||
* [IEx] Improve error message for `IEx.Helpers.r` when module does not exist
|
||||
* [Mix] Ensure `deps.get` updates origin if lock origin and dep origin do not match
|
||||
* [Mix] Use relative symlinks in _build
|
||||
* [Typespec] Fix conversion of unary ops from typespec format to ast
|
||||
* [Typespec] Fix handling of `tuple()` and `{}`
|
||||
|
||||
* Deprecations
|
||||
* [Kernel] Do not leak clause heads. Previously, a variable defined in a case/receive head clauses would leak to the outer scope. This behaviour is deprecated and will be removed in the next release.
|
||||
* [Kernel] Deprecate `__FILE__` in favor of `__DIR__` or `__ENV__.file`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [GenFSM] GenServer now stops on unknown event/sync_event requests
|
||||
* [GenServer] GenServer now stops on unknown call/cast requests
|
||||
* [Kernel] Change how `->` is represented in AST. Now each clause is represented by its own AST node which makes composition easier. See commit 51aef55 for more information.
|
||||
|
||||
# v0.12.0 (2013-12-15)
|
||||
|
||||
* Enhancements
|
||||
* [Exception] Allow `exception/1` to be overridden and promote it as the main mechanism to customize exceptions
|
||||
* [File] Add `File.stream_to!/3`
|
||||
* [Float] Add `Float.floor/1`, `Float.ceil/1` and `Float.round/3`
|
||||
* [Kernel] Add `List.delete_at/2` and `List.updated_at/3`
|
||||
* [Kernel] Add `Enum.reverse/2`
|
||||
* [Kernel] Implement `defmodule/2`, `@/1`, `def/2` and friends in Elixir itself. `case/2`, `try/2` and `receive/1` have been made special forms. `var!/1`, `var!/2` and `alias!/1` have also been implemented in Elixir and demoted from special forms
|
||||
* [Record] Support dynamic fields in `defrecordp`
|
||||
* [Stream] Add `Stream.resource/3`
|
||||
* [Stream] Add `Stream.zip/2`, `Stream.filter_map/3`, `Stream.each/2`, `Stream.take_every/2`, `Stream.chunk/2`, `Stream.chunk/3`, `Stream.chunk/4`, `Stream.chunk_by/2`, `Stream.scan/2`, `Stream.scan/3`, `Stream.uniq/2`, `Stream.after/2` and `Stream.run/1`
|
||||
* [Stream] Support `Stream.take/2` and `Stream.drop/2` with negative counts
|
||||
|
||||
* Bug fixes
|
||||
* [HashDict] Ensure a `HashDict` stored in an attribute can be accessed via the attribute
|
||||
* [Enum] Fix bug in `Enum.chunk/4` where you'd get an extra element when the enumerable was a multiple of the counter and a pad was given
|
||||
* [IEx] Ensure `c/2` helper works with full paths
|
||||
* [Kernel] `quote location: :keep` now only affects definitions in order to keep the proper trace in definition exceptions
|
||||
* [Mix] Also symlink `include` directories in _build dependencies
|
||||
* [Version] Fix `Version.match?/2` with `~>` and versions with alphanumeric build info (like `-dev`)
|
||||
|
||||
* Deprecations
|
||||
* [Enum] `Enumerable.count/1` and `Enumerable.member?/2` should now return tagged tuples. Please see `Enumerable` docs for more info
|
||||
* [Enum] Deprecate `Enum.chunks/2`, `Enum.chunks/4` and `Enum.chunks_by/2` in favor of `Enum.chunk/2`, `Enum.chunk/4` and `Enum.chunk_by/2`
|
||||
* [File] `File.binstream!/3` is deprecated. Simply use `File.stream!/3` which is able to figure out if `stream` or `binstream` operations should be used
|
||||
* [Macro] `Macro.extract_args/1` is deprecated in favor of `Macro.decompose_call/1`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Enum] Behaviour of `Enum.drop/2` and `Enum.take/2` has been switched when given negative counts
|
||||
* [Enum] Behaviour of `Enum.zip/2` has been changed to stop as soon as the first enumerable finishes
|
||||
* [Enum] `Enumerable.reduce/3` protocol has changed to support suspension. Please see `Enumerable` docs for more info
|
||||
* [Mix] Require `:escript_main_module` to be set before generating escripts
|
||||
* [Range] `Range.Iterator` protocol has changed in order to work with the new `Enumerable.reduce/3`. Please see `Range.Iterator` docs for more info
|
||||
* [Stream] The `Stream.Lazy` structure has changed to accumulate functions and accumulators as we go (its inspected representation has also changed)
|
||||
* [Typespec] `when` clauses were moved to the outer part of the spec and should be in the keywords format. So `add(a, b) when is_subtype(a, integer) and is_subtype(b, integer) :: integer` should now be written as `add(a, b) :: integer when a: integer, b: integer`
|
||||
|
||||
# v0.11.2 (2013-11-14)
|
||||
|
||||
* Enhancements
|
||||
* [Mix] Add `mix iex` that redirects users to the proper `iex -S mix` command
|
||||
* [Mix] Support `build_per_environment: true` in project configuration that manages a separete build per environment, useful when you have per-environment behaviour/compilation
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Mix] Mix now compiles files to `_build`. Projects should update just fine, however documentation and books may want to update to the latest information
|
||||
|
||||
# v0.11.1 (2013-11-07)
|
||||
|
||||
* Enhancements
|
||||
* [Mix] Improve dependency convergence by explicitly checking each requirement instead of expecting all requirements to be equal
|
||||
* [Mix] Support optional dependencies with `optional: true`. Optional dependencies are downloaded for the current project but they are automatically skipped when such project is used as a dependency
|
||||
|
||||
* Bug fixes
|
||||
* [Kernel] Set compilation status per ParallelCompiler and not globally
|
||||
* [Mix] Ensure Mix does not load previous dependencies versions before `deps.get`/`deps.update`
|
||||
* [Mix] Ensure umbrella apps are sorted before running recursive commands
|
||||
* [Mix] Ensure umbrella apps run in the same environment as the parent project
|
||||
* [Mix] Ensure dependency tree is topsorted before compiling
|
||||
* [Mix] Raise error when duplicated projects are pushed into the stack
|
||||
* [URI] Allow lowercase escapes in URI
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Mix] Setting `:load_paths` in your project configuration is deprecated
|
||||
|
||||
# v0.11.0 (2013-11-02)
|
||||
|
||||
* Enhancements
|
||||
* [Code] Eval now returns variables from other contexts
|
||||
* [Dict] Document and enforce all dicts use the match operator (`===`) when checking for keys
|
||||
* [Enum] Add `Enum.slice/2` with a range
|
||||
* [Enum] Document and enforce `Enum.member?/2` to use the match operator (`===`)
|
||||
* [IEx] Split `IEx.Evaluator` from `IEx.Server` to allow custom evaluators
|
||||
* [IEx] Add support for `IEx.pry` which halts a given process for inspection
|
||||
* [IO] Add specs and allow some IO APIs to receive any data that implements `String.Chars`
|
||||
* [Kernel] Improve stacktraces on command line interfaces
|
||||
* [Kernel] Sigils can now handle balanced tokens as in `%s(f(o)o)`
|
||||
* [Kernel] Emit warnings when an alias is not used
|
||||
* [Macro] Add `Macro.pipe/3` and `Macro.unpipe/1` for building pipelines
|
||||
* [Mix] Allow umbrella children to share dependencies between them
|
||||
* [Mix] Allow mix to be escriptize'd
|
||||
* [Mix] Speed mix projects compilation by relying on more manifests information
|
||||
* [Protocol] Protocols now provide `impl_for/1` and `impl_for!/1` functions which receive a structure and returns its respective implementation, otherwise returns nil or an error
|
||||
* [Set] Document and enforce all sets use the match operator (`===`) when checking for keys
|
||||
* [String] Update to Unicode 6.3.0
|
||||
* [String] Add `String.slice/2` with a range
|
||||
|
||||
* Bug fixes
|
||||
* [Exception] Ensure `defexception` fields can be set dynamically
|
||||
* [Kernel] Guarantee aliases hygiene is respected when the current module name is not known upfront
|
||||
* [Kernel] `Kernel.access/2` no longer flattens lists
|
||||
* [Mix] Ensure cyclic dependencies are properly handled
|
||||
* [String] Implement the extended grapheme cluster algorithm for `String` operations
|
||||
|
||||
* Deprecations
|
||||
* [Kernel] `pid_to_list/1`, `list_to_pid/1`, `binary_to_atom/2`, `binary_to_existing_atom/2` and `atom_to_binary/2` are deprecated in favor of their counterparts in the `:erlang` module
|
||||
* [Kernel] `insert_elem/3` and `delete_elem/2` are deprecated in favor of `Tuple.insert_at/3` and `Tuple.delete_at/2`
|
||||
* [Kernel] Use of `in` inside matches (as in `x in [1,2,3] -> x`) is deprecated in favor of the guard syntax (`x when x in [1,2,3]`)
|
||||
* [Macro] `Macro.expand_all/2` is deprecated
|
||||
* [Protocol] `@only` and `@except` in protocols are now deprecated
|
||||
* [Protocol] Protocols no longer fallback to `Any` out of the box (this functionality needs to be explicitly enabled by setting `@fallback_to_any` to true)
|
||||
* [String] `String.to_integer/1` and `String.to_float/1` are deprecated in favor of `Integer.parse/1` and `Float.parse/1`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [CLI] Reading `.elixirrc` has been dropped in favor of setting env vars
|
||||
* [Kernel] `Kernel.access/2` now expects the second argument to be a compile time list
|
||||
* [Kernel] `fn -> end` quoted expression is no longer wrapped in a `do` keyword
|
||||
* [Kernel] Quoted variables from the same module must be explicitly shared. Previously, if a function returned `quote do: a = 1`, another function from the same module could access it as `quote do: a`. This has been fixed and the variables must be explicitly shared with `var!(a, __MODULE__)`
|
||||
* [Mix] Umbrella apps now treat children apps as dependencies. This means all dependencies will be checked out in the umbrela `deps` directory. On upgrade, child apps need to point to the umbrella project by setting `deps_path: "../../deps_path", lockfile: "../../mix.lock"` in their project config
|
||||
* [Process] `Process.group_leader/2` args have been reversed so the "subject" comes first
|
||||
* [Protocol] Protocol no longer dispatches to `Number`, but to `Integer` and `Float`
|
||||
|
||||
# v0.10.3 (2013-10-02)
|
||||
|
||||
* Enhancements
|
||||
* [Enum] Add `Enum.take_every/2`
|
||||
* [IEx] IEx now respects signals sent from the Ctrl+G menu
|
||||
* [Kernel] Allow documentation for types with `@typedoc`
|
||||
* [Mix] Allow apps to be selected in umbrella projects
|
||||
* [Record] Generated record functions `new` and `update` also take options with strings as keys
|
||||
* [Stream] Add `Stream.unfold/1`
|
||||
|
||||
* Bug fixes
|
||||
* [Dict] Fix a bug when a HashDict was marked as equal when one was actually a subset of the other
|
||||
* [EEx] Solve issue where `do` blocks inside templates were not properly aligned
|
||||
* [ExUnit] Improve checks and have better error reports on poorly aligned doctests
|
||||
* [Kernel] Fix handling of multiple heredocs on the same line
|
||||
* [Kernel] Provide better error messages for match, guard and quoting errors
|
||||
* [Kernel] Make `Kernel.raise/2` a macro to avoid messing up stacktraces
|
||||
* [Kernel] Ensure `&()` works on quoted blocks with only one expression
|
||||
* [Mix] Address an issue where a dependency was not compiled in the proper order when specified in different projects
|
||||
* [Mix] Ensure `compile: false` is a valid mechanism for disabling the compilation of dependencies
|
||||
* [Regex] Fix bug on `Regex.scan/3` when capturing groups and the regex has no groups
|
||||
* [String] Fix a bug with `String.split/2` when given an empty pattern
|
||||
* [Typespec] Guarantee typespecs error reports point to the proper line
|
||||
|
||||
* Deprecations
|
||||
* [Kernel] The previous partial application syntax (without the `&` operator) has now been deprecated
|
||||
* [Regex] `Regex.captures/3` is deprecated in favor of `Regex.named_captures/3`
|
||||
* [String] `String.valid_codepoint?/1` is deprecated in favor of pattern matching with `<<_ :: utf8 >>`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [IEx] The `r/0` helper has been removed as it caused surprising behaviour when many modules with dependencies were accumulated
|
||||
* [Mix] `Mix.Version` was renamed to `Version`
|
||||
* [Mix] `File.IteratorError` was renamed to `IO.StreamError`
|
||||
* [Mix] `mix new` now defaults to the `--sup` option, use `--bare` to get the previous behaviour
|
||||
|
||||
# v0.10.2 (2013-09-03)
|
||||
|
||||
* Enhancements
|
||||
* [CLI] Add `--verbose` to elixirc, which now is non-verbose by default
|
||||
* [Dict] Add `Dict.Behaviour` as a convenience to create your own dictionaries
|
||||
* [Enum] Add `Enum.split/2`, `Enum.reduce/2`, `Enum.flat_map/2`, `Enum.chunk/2`, `Enum.chunk/4`, `Enum.chunk_by/2`, `Enum.concat/1` and `Enum.concat/2`
|
||||
* [Enum] Support negative indices in `Enum.at/fetch/fetch!`
|
||||
* [ExUnit] Show failures on CLIFormatter as soon as they pop up
|
||||
* [IEx] Allow for strings in `h` helper
|
||||
* [IEx] Helpers `r` and `c` can handle erlang sources
|
||||
* [Integer] Add `odd?/1` and `even?/1`
|
||||
* [IO] Added support to specifying a number of bytes to stream to `IO.stream`, `IO.binstream`, `File.stream!` and `File.binstream!`
|
||||
* [Kernel] Include file and line on error report for overriding an existing function/macro
|
||||
* [Kernel] Convert external functions into quoted expressions. This allows record fields to contain functions as long as they point to an `&Mod.fun/arity`
|
||||
* [Kernel] Allow `foo?` and `bar!` as valid variable names
|
||||
* [List] Add `List.replace_at/3`
|
||||
* [Macro] Improve printing of the access protocol on `Macro.to_string/1`
|
||||
* [Macro] Add `Macro.to_string/2` to support annotations on the converted string
|
||||
* [Mix] Automatically recompile a project if the Elixir version changes
|
||||
* [Path] Add `Path.relative_to_cwd/2`
|
||||
* [Regex] Allow erlang `re` options when compiling Elixir regexes
|
||||
* [Stream] Add `Stream.concat/1`, `Stream.concat/2` and `Stream.flat_map/2`
|
||||
* [String] Add regex pattern support to `String.replace/3`
|
||||
* [String] Add `String.ljust/2`, `String.rjust/2`, `String.ljust/3` and `String.rjust/3`
|
||||
* [URI] `URI.parse/1` supports IPv6 addresses
|
||||
|
||||
* Bug fixes
|
||||
* [Behaviour] Do not compile behaviour docs if docs are disabled on compilation
|
||||
* [ExUnit] Doctests no longer eat too much space and provides detailed reports for poorly indented lines
|
||||
* [File] Fix a bug where `File.touch(file, datetime)` was not setting the proper datetime when the file did not exist
|
||||
* [Kernel] Limit `inspect` results to 50 items by default to avoid printing too much data
|
||||
* [Kernel] Return a readable error on oversized atoms
|
||||
* [Kernel] Allow functions ending with `?` or `!` to be captured
|
||||
* [Kernel] Fix default shutdown of child supervisors to `:infinity`
|
||||
* [Kernel] Fix regression when calling a function/macro ending with bang, followed by `do/end` blocks
|
||||
* [List] Fix bug on `List.insert_at/3` that added the item at the wrong position for negative indexes
|
||||
* [Macro] `Macro.escape/2` can now escape improper lists
|
||||
* [Mix] Fix `Mix.Version` matching on pre-release info
|
||||
* [Mix] Ensure `watch_exts` trigger full recompilation on change with `mix compile`
|
||||
* [Mix] Fix regression on `mix clean --all`
|
||||
* [String] `String.strip/2` now supports removing unicode characters
|
||||
* [String] `String.slice/3` still returns the proper result when there is no length to be extracted
|
||||
* [System] `System.get_env/0` now returns a list of tuples as previously advertised
|
||||
|
||||
* Deprecations
|
||||
* [Dict] `Dict.update/3` is deprecated in favor of `Dict.update!/3`
|
||||
* [Enum] `Enum.min/2` and `Enum.max/2` are deprecated in favor of `Enum.min_by/2` and `Enum.max_by/2`
|
||||
* [Enum] `Enum.join/2` and `Enum.map_join/3` with a char list are deprecated
|
||||
* [IO] `IO.stream(device)` and `IO.binstream(device)` are deprecated in favor of `IO.stream(device, :line)` and `IO.binstream(device, :line)`
|
||||
* [Kernel] `list_to_binary/1`, `binary_to_list/1` and `binary_to_list/3` are deprecated in favor of `String.from_char_list!/1` and `String.to_char_list!/1` for characters and `:binary.list_to_bin/1`, `:binary.bin_to_list/1` and `:binary.bin_to_list/3` for bytes
|
||||
* [Kernel] `to_binary/1` is deprecated in favor of `to_string/1`
|
||||
* [Kernel] Deprecate `def/4` and friends in favor of `def/2` with unquote and friends
|
||||
* [Kernel] Deprecate `%b` and `%B` in favor of `%s` and `%S`
|
||||
* [List] `List.concat/2` is deprecated in favor of `Enum.concat/2`
|
||||
* [Macro] `Macro.unescape_binary/1` and `Macro.unescape_binary/2` are deprecated in favor of `Macro.unescape_string/1` and `Macro.unescape_string/2`
|
||||
* [Mix] `:umbrella` option for umbrella paths has been deprecated in favor of `:in_umbrella`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [IO] IO functions now only accept iolists as arguments
|
||||
* [Kernel] `Binary.Chars` was renamed to `String.Chars`
|
||||
* [Kernel] The previous ambiguous import syntax `import :functions, Foo` was removed in favor of `import Foo, only: :functions`
|
||||
* [OptionParser] `parse` and `parse_head` now returns a tuple with three elements instead of two
|
||||
|
||||
# v0.10.1 (2013-08-03)
|
||||
|
||||
* Enhancements
|
||||
* [Behaviour] Add support for `defmacrocallback/1`
|
||||
* [Enum] Add `Enum.shuffle/1`
|
||||
* [ExUnit] The `:trace` option now also reports run time for each test
|
||||
* [ExUnit] Add support for `:color` to enable/disable ANSI coloring
|
||||
* [IEx] Add the `clear` helper to clear the screen.
|
||||
* [Kernel] Add the capture operator `&`
|
||||
* [Kernel] Add support for `GenFSM.Behaviour`
|
||||
* [Kernel] Functions now points to the module and function they were defined when inspected
|
||||
* [Kernel] A documentation attached to a function that is never defined now prints warnings
|
||||
* [List] Add `List.keysort/2`
|
||||
* [Mix] `:test_helper` project configuration did not affect `mix test` and was therefore removed. A `test/test_helper.exs` file is still necessary albeit it doesn't need to be automatically required in each test file
|
||||
* [Mix] Add manifests for yecc, leex and Erlang compilers, making it easier to detect dependencies in between compilers and providing a more useful clean behaviour
|
||||
* [Mix] `mix help` now outputs information about the default mix task
|
||||
* [Mix] Add `--no-deps-check` option to `mix run`, `mix compile` and friends to not check dependency status
|
||||
* [Mix] Add support for `MIX_GIT_FORCE_HTTPS` system environment that forces HTTPS for known providers, useful when the regular git port is blocked. This configuration does not affect the `mix.lock` results
|
||||
* [Mix] Allow coverage tool to be pluggable via the `:test_coverage` configuration
|
||||
* [Mix] Add `mix cmd` as a convenience to run a command recursively in child apps in an umbrella application
|
||||
* [Mix] Support `umbrella: true` in dependencies as a convenience for setting up umbrella path deps
|
||||
* [Mix] `mix run` now behaves closer to the `elixir` command and properly mangles the ARGV
|
||||
* [String] Add `Regex.scan/3` now supports capturing groups
|
||||
* [String] Add `String.reverse/1`
|
||||
|
||||
* Bug fixes
|
||||
* [Behaviour] Ensure callbacks are stored in the definition order
|
||||
* [CLI] Speed up boot time on Elixir .bat files
|
||||
* [IEx] Reduce cases where IEx parser can get stuck
|
||||
* [Kernel] Improve error messages when the use of an operator has no effect
|
||||
* [Kernel] Fix a bug where warnings were not being generated when imported macros conflicted with local functions or macros
|
||||
* [Kernel] Document that `on_definition` can only be a function as it is evaluated inside the function context
|
||||
* [Kernel] Ensure `%w` sigils with no interpolation are fully expanded at compile time
|
||||
* [Mix] `mix deps.update`, `mix deps.clean` and `mix deps.unlock` no longer change all dependencies unless `--all` is given
|
||||
* [Mix] Always run ` mix loadpaths` on `mix app.start`, even if `--no-compile` is given
|
||||
* [OptionParser] Do not add boolean flags to the end result if they were not given
|
||||
* [OptionParser] Do not parse non-boolean flags as booleans when true or false are given
|
||||
* [OptionParser] Ensure `:keep` and `:integer`|`:float` can be given together as options
|
||||
* [OptionParser] Ensure `--no-flag` sets `:flag` to false when `:flag` is a registered boolean switch
|
||||
|
||||
* Deprecations
|
||||
* [Kernel] `function(Mod.fun/arity)` and `function(fun/arity)` are deprecated in favor of `&Mod.fun/arity` and `&fun/arity`
|
||||
* [Kernel] `function/3` is deprecated in favor of `Module.function/3`
|
||||
* [Kernel] `Kernel.ParallelCompiler` now receives a set of callbacks instead of a single one
|
||||
* [Mix] `:test_coverage` option now expect keywords arguments and the `--cover` flag is now treated as a boolean
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Regex] `Regex.scan/3` now always returns a list of lists, normalizing the result, instead of list with mixed lists and binaries
|
||||
* [System] `System.halt/2` was removed since the current Erlang implementation of such function is bugged
|
||||
|
||||
# v0.10.0 (2013-07-15)
|
||||
|
||||
* Enhancements
|
||||
* [ExUnit] Support `trace: true` option which gives detailed reporting on test runs
|
||||
* [HashDict] Optimize `HashDict` to store pairs in a cons cell reducing storage per key by half
|
||||
* [Kernel] Add pretty printing support for inspect
|
||||
* [Kernel] Add document algebra library used as the foundation for pretty printing
|
||||
* [Kernel] Add `defrecordp/3` that enables specifying the first element of the tuple
|
||||
* [Kernel] Add the `Set` API and a hash based implementation via `HashSet`
|
||||
* [Kernel] Add `Stream` as composable, lazy-enumerables
|
||||
* [Mix] `mix archive` now includes the version of the generated archive
|
||||
* [Mix] Mix now requires explicit dependency overriding to be given with `override: true`
|
||||
* [Mix] Projects can now define an `:elixir` key to outline supported Elixir versions
|
||||
* [Typespec] Improve error messages to contain file, line and the typespec itself
|
||||
|
||||
* Bug fixes
|
||||
* [CLI] Elixir can now run on Unix directories with `:` in its path
|
||||
* [Kernel] `match?/2` does not leak variables to outer scope
|
||||
* [Kernel] Keep `head|tail` format when splicing at the tail
|
||||
* [Kernel] Ensure variables defined in the module body are not passed to callbacks
|
||||
* [Mix] On dependencies conflict, show from where each source is coming from
|
||||
* [Mix] Empty projects no longer leave empty ebin files on `mix compile`
|
||||
* [Module] Calling `Module.register_attribute/3` no longer automatically changes it to persisted or accumulated
|
||||
|
||||
* Deprecations
|
||||
* [Enum] Receiving the index of iteration in `Enum.map/2` and `Enum.each/2` is deprecated in favor of `Stream.with_index/1`
|
||||
* [File] `File.iterator/1` and `File.biniterator/1` are deprecated in favor of `IO.stream/1` and `IO.binstream/1`
|
||||
* [File] `File.iterator!/2` and `File.biniterator!/2` are deprecated in favor of `File.stream!/2` and `File.binstream!/2`
|
||||
* [Kernel] Deprecate recently added `quote binding: ...` in favor of the clearer `quote bind_quoted: ...`
|
||||
* [Kernel] Deprecate `Kernel.float/1` in favor of a explicit conversion
|
||||
* [Mix] Deprecate `mix run EXPR` in favor of `mix run -e EXPR`
|
||||
* [Record] `Record.__index__/2` deprecated in favor of `Record.__record__(:index, key)`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Kernel] The `Binary.Inspect` protocol has been renamed to `Inspect`
|
||||
* [Kernel] Tighten up the grammar rules regarding parentheses omission, previously the examples below would compile but now they raise an error message:
|
||||
|
||||
do_something 1, is_list [], 3
|
||||
[1, is_atom :foo, 3]
|
||||
|
||||
* [Module] Calling `Module.register_attribute/3` no longer automatically changes it to persisted or accumulated
|
||||
* [Record] First element of a record via `defrecordp` is now the `defrecordp` name and no longer the current atom
|
||||
* [URI] Remove custom URI parsers in favor of `URI.default_port/2`
|
||||
|
||||
# v0.9.3 (2013-06-23)
|
||||
|
||||
* Enhancements
|
||||
* [File] Add `File.chgrp`, `File.chmod` and `File.chown`
|
||||
* [Kernel] Add `--warnings-as-errors` to Elixir's compiler options
|
||||
* [Kernel] Print warnings to stderr
|
||||
* [Kernel] Warn on undefined module attributes
|
||||
* [Kernel] Emit warning for `x in []` in guards
|
||||
* [Kernel] Add `binding/0` and `binding/1` for retrieving bindings
|
||||
* [Kernel] `quote` now allows a binding as an option
|
||||
* [Macro] Add `Macro.expand_once/2` and `Macro.expand_all/2`
|
||||
* [Mix] Implement `Mix.Version` for basic versioning semantics
|
||||
* [Mix] Support creation and installation of archives (.ez files)
|
||||
* [Mix] `github: ...` shortcut now uses the faster `git` schema instead of `https`
|
||||
* [Record] Allow types to be given to `defrecordp`
|
||||
|
||||
* Bug fixes
|
||||
* [Kernel] The elixir executable on Windows now supports the same options as the UNIX one
|
||||
* [Kernel] Improve error messages on default clauses clash
|
||||
* [Kernel] `__MODULE__.Foo` now returns `Foo` when outside of a Module
|
||||
* [Kernel] Improve error messages when default clauses from different definitions collide
|
||||
* [Kernel] `^x` variables should always refer to the value before the expression
|
||||
* [Kernel] Allow `(x, y) when z` in function clauses and try expressions
|
||||
* [Mix] Mix now properly evaluates rebar scripts
|
||||
|
||||
* Deprecations
|
||||
* [Code] `Code.string_to_ast/1` has been deprecated in favor of `Code.string_to_quoted/1`
|
||||
* [Macro] `Macro.to_binary/1` has been deprecated in favor of `Macro.to_string/1`
|
||||
* [Typespec] Deprecate `(fun(...) -> ...)` in favor of `(... -> ...)`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Bitwise] Precedence of operators used by the Bitwise module were changed, check `elixir_parser.yrl` for more information
|
||||
* [File] `rm_rf` and `cp_r` now returns a tuple with three elements on failures
|
||||
* [Kernel] The quoted representation for `->` clauses changed from a tuple with two elements to a tuple with three elements to support metadata
|
||||
* [Kernel] Sigils now dispatch to `sigil_$` instead of `__$__` where `$` is the sigil character
|
||||
* [Macro] `Macro.expand/2` now expands until final form. Although this is backwards incompatible, it is very likely you do not need to change your code, since expansion until its final form is recommended, particularly if you are expecting an atom out of it
|
||||
* [Mix] No longer support beam files on `mix local`
|
||||
|
||||
# v0.9.2 (2013-06-13)
|
||||
|
||||
* Enhancements
|
||||
* [ExUnit] `capture_io` now captures prompt by default
|
||||
* [Mix] Automatically import git dependencies from Rebar
|
||||
* [Mix] Support for dependencies directly from the umbrella application
|
||||
* [Regex] Add `Regex.escape`
|
||||
* [String] Add `String.contains?`
|
||||
* [URI] Implement `Binary.Chars` (aka `to_binary`) for `URI.Info`
|
||||
|
||||
* Bug fixes
|
||||
* [HashDict] Ensure HashDict uses exact match throughout its implementation
|
||||
* [IEx] Do not interpret ANSI codes in IEx results
|
||||
* [IEx] Ensure `--cookie` is set before accessing remote shell
|
||||
* [Kernel] Do not ignore nil when dispatching protocols to avoid infinite loops
|
||||
* [Mix] Fix usage of shell expressions in `Mix.Shell.cmd`
|
||||
* [Mix] Start the application by default on escripts
|
||||
|
||||
* Deprecations
|
||||
* [Regex] `Regex.index/2` is deprecated in favor `Regex.run/3`
|
||||
* [Kernel] `super` no longer supports implicit arguments
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Kernel] The `=~` operator now returns true or false instead of an index
|
||||
|
||||
# v0.9.1 (2013-05-30)
|
||||
|
||||
* Enhancements
|
||||
* [IEx] Limit the number of entries kept in history and allow it to be configured
|
||||
* [Kernel] Add `String.start_with?` and `String.end_with?`
|
||||
* [Typespec] Allow keywords, e.g. `[foo: integer, bar: boolean | module]`, in typespecs
|
||||
|
||||
* Bug fixes
|
||||
* [Dict] `Enum.to_list` and `Dict.to_list` now return the same results for dicts
|
||||
* [IEx] Enable shell customization via the `IEx.Options` module
|
||||
* [Kernel] Fix a bug where `unquote_splicing` did not work on the left side of a stab op
|
||||
* [Kernel] Unused functions with cyclic dependencies are now also warned as unused
|
||||
* [Mix] Fix a bug where `mix deps.get` was not retrieving nested dependencies
|
||||
* [Record] Fix a bug where nested records cannot be defined
|
||||
* [Record] Fix a bug where a record named Record cannot be defined
|
||||
|
||||
# v0.9.0 (2013-05-23)
|
||||
|
||||
* Enhancements
|
||||
* [ExUnit] `ExUnit.CaptureIO` now accepts an input to be used during capture
|
||||
* [IEx] Add support for .iex files that are loaded during shell's boot process
|
||||
* [IEx] Add `import_file/1` helper
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Enum] `Enum.Iterator` was replaced by the more composable and functional `Enumerable` protocol which supports reductions
|
||||
* [File] `File.iterator/1` and `File.biniterator/1` have been removed in favor of the safe `File.iterator!/1` and `File.biniterator!/1` ones
|
||||
* [Kernel] Erlang R15 is no longer supported
|
||||
* [Kernel] Elixir modules are now represented as `Elixir.ModuleName` (using `.` instead of `-` as separator)
|
||||
|
||||
# v0.8.3 (2013-05-22)
|
||||
|
||||
* Enhancements
|
||||
* [CLI] Flags `-p` and `-pr` fails if pattern match no files
|
||||
* [CLI] Support `--hidden` and `--cookie` flags for distributed Erlang
|
||||
* [Enum] Add `Enum.to_list/1`, `Enum.member?/2`, `Enum.uniq/2`, `Enum.max/1`, `Enum.max/2`, `Enum.min/1` and `Enum.min/2`
|
||||
* [ExUnit] Add `ExUnit.CaptureIO` for IO capturing during tests
|
||||
* [ExUnit] Consider load time on ExUnit time reports
|
||||
* [IEx] Support `ls` with colored output
|
||||
* [IEx] Add `#iex:break` to break incomplete expressions
|
||||
* [Kernel] Add `Enum.at`, `Enum.fetch` and `Enum.fetch!`
|
||||
* [Kernel] Add `String.to_integer` and `String.to_float`
|
||||
* [Kernel] Add `Dict.take`, `Dict.drop`, `Dict.split`, `Dict.pop` and `Dict.fetch!`
|
||||
* [Kernel] Many optimizations for code compilation
|
||||
* [Kernel] `in` can be used with right side expression outside guards
|
||||
* [Kernel] Add `Node.get_cookie/0` and `Node.set_cookie/2`
|
||||
* [Kernel] Add `__DIR__`
|
||||
* [Kernel] Expand macros and attributes on quote, import, alias and require
|
||||
* [Kernel] Improve warnings related to default arguments
|
||||
* [Keyword] Add `Keyword.delete_first/2`
|
||||
* [Mix] Add `local.rebar` to download a local copy of rebar, and change `deps.compile` to use it if needed
|
||||
* [Mix] Support umbrella applications
|
||||
* [Mix] Load beam files available at `MIX_PATH` on CLI usage
|
||||
* [String] Add `String.valid?` and `String.valid_character?`
|
||||
|
||||
* Bug fixes
|
||||
* [ExUnit] Handle exit messages from in ExUnit
|
||||
* [ExUnit] Failures on ExUnit's setup_all now invalidates all tests
|
||||
* [Kernel] Ensure we don't splice keyword args unecessarily
|
||||
* [Kernel] Private functions used by private macros no longer emit an unused warning
|
||||
* [Kernel] Ensure Elixir won't trip on empty receive blocks
|
||||
* [Kernel] `String.slice` now returns an empty string when out of range by 1
|
||||
* [Mix] Generate manifest files after compilation to avoid depending on directory timestamps and to remove unused .beam files
|
||||
* [Path] `Path.expand/2` now correctly expands `~` in the second argument
|
||||
* [Regex] Fix badmatch with `Regex.captures(%r/(.)/g, "cat")`
|
||||
* [URI] Downcase host and scheme and URIs
|
||||
|
||||
* Deprecations
|
||||
* [Code] `Code.eval` is deprecated in favor of `Code.eval_string`
|
||||
* [Exception] `Exception.format_entry` is deprecated in favor of `Exception.format_stacktrace_entry`
|
||||
* [ExUnit] `assert left inlist right` is deprecated in favor of `assert left in right`
|
||||
* [IO] `IO.getb` is deprecated in favor of `IO.getn`
|
||||
* [List] `List.member?/2` is deprecated in favor of `Enum.member?/2`
|
||||
* [Kernel] `var_context` in quote was deprecated in favor of `context`
|
||||
* [Kernel] `Enum.at!` and `Dict.get!` is deprecated in favor of `Enum.fetch!` and `Dict.fetch!`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Dict] `List.Dict` was moved to `ListDict`
|
||||
* [IO] `IO.gets`, `IO.getn` and friends now return binaries when reading from stdio
|
||||
* [Kernel] Precedence of `|>` has changed to lower to support constructs like `1..5 |> Enum.to_list`
|
||||
* [Mix] `mix escriptize` now receives arguments as binaries
|
||||
|
||||
# v0.8.2 (2013-04-20)
|
||||
|
||||
* Enhancements
|
||||
* [ExUnit] Use ANSI escape codes in CLI output
|
||||
* [ExUnit] Include suite run time on CLI results
|
||||
* [ExUnit] Add support to doctests, allowing test cases to be generated from code samples
|
||||
* [File] Add `File.ls` and `File.ls!`
|
||||
* [IEx] Support `pwd` and `cd` helpers
|
||||
* [Kernel] Better error reporting for invalid bitstring generators
|
||||
* [Kernel] Improve meta-programming by allowing `unquote` on `def/2`, `defp/2`, `defmacro/2` and `defmacrop/2`
|
||||
* [Kernel] Add support to R16B new functions: `insert_elem/3` and `delete_elem/2`
|
||||
* [Kernel] Import conflicts are now lazily handled. If two modules import the same functions, it will fail only if the function is invoked
|
||||
* [Mix] Support `--cover` on mix test and `test_coverage` on Mixfiles
|
||||
* [Record] Each record now provides `Record.options` with the options supported by its `new` and `update` functions
|
||||
|
||||
* Bug fixes
|
||||
* [Binary] inspect no longer escapes standalone hash `#`
|
||||
* [IEx] The `h` helper can now retrieve docs for special forms
|
||||
* [Kernel] Record optimizations were not being triggered in functions inside the record module
|
||||
* [Kernel] Aliases defined inside macros should be carried over
|
||||
* [Kernel] Fix a bug where nested records could not use the Record[] syntax
|
||||
* [Path] Fix a bug on `Path.expand` when expanding paths starting with `~`
|
||||
|
||||
* Deprecations
|
||||
* [Kernel] `setelem/3` is deprecated in favor of `set_elem/3`
|
||||
* [Kernel] `function(:is_atom, 1)` is deprecated in favor of `function(is_atom/1)`
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Kernel] `unquote` now only applies to the closest quote. If your code contains a quote that contains another quote that calls unquote, it will no longer work. Use `Macro.escape` instead and pass your quoted contents up in steps, for example:
|
||||
|
||||
quote do
|
||||
quote do: unquote(x)
|
||||
end
|
||||
|
||||
should become:
|
||||
|
||||
quote do
|
||||
unquote(Macro.escape(x))
|
||||
end
|
||||
|
||||
# v0.8.1 (2013-02-17)
|
||||
|
||||
* Enhancements
|
||||
* [ExUnit] Tests can now receive metadata set on setup/teardown callbacks
|
||||
* [ExUnit] Add support to ExUnit.CaseTemplate to share callbacks in between test cases
|
||||
* [IO] Add `IO.ANSI` to make it easy to write ANSI escape codes
|
||||
* [Kernel] Better support for Unicode lists
|
||||
* [Kernel] Reduce variables footprint in `case`/`receive` clauses
|
||||
* [Kernel] Disable native compilation when on_load attributes is present to work around an Erlang bug
|
||||
* [Macro] `Macro.expand` also considers macros from the current `__ENV__` module
|
||||
* [Mix] Improve support for compilation of `.erl` files
|
||||
* [Mix] Add support for compilation of `.yrl` and `.xrl` files
|
||||
* [OptionParser] Switches are now overridden by default but can be kept in order if chosen
|
||||
* [Typespec] Better error reporting for invalid typespecs
|
||||
|
||||
* Bug fixes
|
||||
* [Mix] Allow Mix projects to be generated with just one letter
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Kernel] `before_compile` and `after_compile` callbacks now receive the environment as first argument instead of the module
|
||||
|
||||
* Deprecations
|
||||
* [ExUnit] Explicitly defined test/setup/teardown functions are deprecated
|
||||
* [Kernel] Tidy up and clean `quote` API
|
||||
* [Kernel] Old `:local.(args)` syntax is deprecated
|
||||
* [Process] `Process.self` is deprecated in favor `Kernel.self`
|
||||
|
||||
# v0.8.0 (2013-01-28)
|
||||
|
||||
* Enhancements
|
||||
* [Binary] Support `<< "string" :: utf8 >>` as in Erlang
|
||||
* [Binary] Support `\a` escape character in binaries
|
||||
* [Binary] Support syntax shortcut for specifying size in bit syntax
|
||||
* [CLI] Support `--app` option to start an application and its dependencies
|
||||
* [Dict] Support `put_new` in `Dict` and `Keyword`
|
||||
* [Dict] Add `ListDict` and a faster `HashDict` implementation
|
||||
* [ExUnit] ExUnit now supports multiple runs in the same process
|
||||
* [ExUnit] Failures in ExUnit now shows a tailored stacktrace
|
||||
* [ExUnit] Introduce `ExUnit.ExpectationError` to provide better error messages
|
||||
* [Kernel] Introduce `Application.Behaviour` to define application module callbacks
|
||||
* [Kernel] Introduce `Supervisor.Behaviour` to define supervisors callbacks
|
||||
* [Kernel] More optimizations were added to Record handling
|
||||
* [Kernel] `?\x` and `?\` are now supported ways to retrieve a codepoint
|
||||
* [Kernel] Octal numbers can now be defined as `0777`
|
||||
* [Kernel] Improve macros hygiene regarding variables, aliases and imports
|
||||
* [Mix] Mix now starts the current application before run, iex, test and friends
|
||||
* [Mix] Mix now provides basic support for compiling `.erl` files
|
||||
* [Mix] `mix escriptize` only generates escript if necessary and accept `--force` and `--no-compile` as options
|
||||
* [Path] Introduce `Path` module to hold filesystem paths related functions
|
||||
* [String] Add `String.capitalize` and `String.slice`
|
||||
* [System] Add `System.tmp_dir`, `System.cwd` and `System.user_home`
|
||||
|
||||
* Bug fixes
|
||||
* [Kernel] `import` with `only` accepts functions starting with underscore
|
||||
* [String] `String.first` and `String.last` return nil for empty binaries
|
||||
* [String] `String.rstrip` and `String.lstrip` now verify if argument is a binary
|
||||
* [Typespec] Support `...` inside typespec's lists
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Kernel] The AST now allows metadata to be attached to each node. This means the second item in the AST is no longer an integer (representing the line), but a keywords list. Code that relies on the line information from AST or that manually generate AST nodes need to be properly updated
|
||||
|
||||
* Deprecations
|
||||
* [Dict] Deprecate `Binary.Dict` and `OrdDict` in favor of `HashDict` and `ListDict`
|
||||
* [File] Deprecate path related functions in favor of the module `Path`
|
||||
* [Kernel] The `/>` operator has been deprecated in favor of `|>`
|
||||
* [Mix] `Mix.Project.sources` is deprecated in favor of `Mix.Project.config_files`
|
||||
* [Mix] `mix iex` is no longer functional, please use `iex -S mix`
|
||||
* [OptionParser] `:flags` option was deprecated in favor of `:switches` to support many types
|
||||
|
||||
# v0.7.2 (2012-12-04)
|
||||
|
||||
* Enhancements
|
||||
* [CLI] `--debug-info` is now true by default
|
||||
* [ExUnit] Make ExUnit exit happen in two steps allowing developers to add custom `at_exit` hooks
|
||||
* [IEx] Many improvements to helpers functions `h/1`, `s/1` and others
|
||||
* [Kernel] Functions defined with `fn` can now handle many clauses
|
||||
* [Kernel] Raise an error if clauses with different arities are defined in the same function
|
||||
* [Kernel] `function` macro now accepts arguments in `M.f/a` and `f/a` formats
|
||||
* [Macro] Improvements to `Macro.to_binary`
|
||||
* [Mix] Mix now echoes the output as it comes when executing external commands such as git or rebar
|
||||
* [Mix] Mix now validates `application` callback's values
|
||||
* [Record] Record accessors are now optimized and can be up to 6x faster in some cases
|
||||
* [String] Support `\xXX` and `\x{HEX}` escape sequences in strings, char lists and regexes
|
||||
|
||||
* Bug fixes
|
||||
* [Bootstrap] Compiling Elixir source no longer fails if environment variables contain utf-8 entries
|
||||
* [IEx] IEx will now wait for all command line options to be processed before starting
|
||||
* [Kernel] Ensure proper stacktraces when showing deprecations
|
||||
|
||||
* Deprecations
|
||||
* [Enum] `Enum.qsort` is deprecated in favor of `Enum.sort`
|
||||
* [List] `List.sort` and `List.uniq` have been deprecated in favor of their `Enum` counterparts
|
||||
* [Record] Default-based generated functions are deprecated
|
||||
* [Typespec] Enhancements and deprecations to the `@spec/@callback` and the fun type syntax
|
||||
|
||||
# v0.7.1 (2012-11-18)
|
||||
|
||||
* Enhancements
|
||||
* [IEx] Only show documented functions and also show docs for default generated functions
|
||||
* [IO] Add `IO.binread`, `IO.binwrite` and `IO.binreadline` to handle raw binary file operations
|
||||
* [ExUnit] Add support for user configuration at `HOME/.ex_unit.exs`
|
||||
* [ExUnit] Add support for custom formatters via a well-defined behaviour
|
||||
* [Kernel] Add support for `defrecordp`
|
||||
* [Kernel] Improved dialyzer support
|
||||
* [Kernel] Improved error messages when creating functions with aliases names
|
||||
* [Mix] Improve SCM behaviour to allow more robust integration
|
||||
* [Mix] Changing deps information on `mix.exs` forces users to fetch new dependencies
|
||||
* [Mix] Support (parallel) requires on mix run
|
||||
* [Mix] Support `-q` when running tests to compile only changed files
|
||||
* [String] Support `String.downcase` and `String.upcase` according to Unicode 6.2.0
|
||||
* [String] Add support for graphemes in `String.length`, `String.at` and others
|
||||
* [Typespec] Support `@opaque` as attribute
|
||||
* [Typespec] Define a default type `t` for protocols and records
|
||||
* [Typespec] Add support for the access protocol in typespecs
|
||||
|
||||
* Bug fixes
|
||||
* [Kernel] Fix an issue where variables inside clauses remained unassigned
|
||||
* [Kernel] Ensure `defoverridable` functions can be referred in many clauses
|
||||
* [Kernel] Allow keywords as function names when following a dot (useful when integrating with erlang libraries)
|
||||
* [File] File is opened by default on binary mode instead of utf-8
|
||||
|
||||
* Deprecations
|
||||
* [Behaviour] `defcallback/1` is deprecated in favor of `defcallback/2` which matches erlang `@callbacks`
|
||||
* [Enum] `Enum.times` is deprecated in favor of using ranges
|
||||
* [System] `halt` moved to `System` module
|
||||
|
||||
# v0.7.0 (2012-10-20)
|
||||
|
||||
* Enhancements
|
||||
* [Behaviour] Add Behaviour with a simple callback DSL to define callbacks
|
||||
* [Binary] Add a Dict binary that converts its keys to binaries on insertion
|
||||
* [Binary] Optimize `Binary.Inspect` and improve inspect for floats
|
||||
* [CLI] Support `--detached` option
|
||||
* [Code] `Code.string_to_ast` supports `:existing_atoms_only` as an option in order to guarantee no new atoms is generated when parsing the code
|
||||
* [EEx] Support `<%%` and `<%#` tags
|
||||
* [ExUnit] Support `after_spawn` callbacks which are invoked after each process is spawned
|
||||
* [ExUnit] Support context data in `setup_all`, `setup`, `teardown` and `teardown_all` callbacks
|
||||
* [IEx] Support `after_spawn` callbacks which are invoked after each process is spawned
|
||||
* [Kernel] Better error messages when invalid options are given to `import`, `alias` or `require`
|
||||
* [Kernel] Allow partial application on literals, for example: `{&1, &2}` to build tuples or `[&1|&2]` to build cons cells
|
||||
* [Kernel] Added `integer_to_binary` and `binary_to_integer`
|
||||
* [Kernel] Added `float_to_binary` and `binary_to_float`
|
||||
* [Kernel] Many improvements to `unquote` and `unquote_splicing`. For example, `unquote(foo).unquote(bar)(args)` is supported and no longer need to be written via `apply`
|
||||
* [Keyword] Keyword list is no longer ordered according to Erlang terms but the order in which they are specified
|
||||
* [List] Add `List.keyreplace` and `List.keystore`
|
||||
* [Macro] Support `Macro.safe_term` which returns `:ok` if an expression does not execute code and is made only of raw data types
|
||||
* [Mix] Add support for environments - the current environment can be set via `MIX_ENV`
|
||||
* [Mix] Add support for handling and fetching dependencies' dependencies
|
||||
* [Module] Support module creation via `Module.create`
|
||||
* [Range] Support decreasing ranges
|
||||
* [Record] Improvements to the Record API, added `Record.defmacros`
|
||||
* [Regex] Add `:return` option to `Regex.run` and `Regex.scan`
|
||||
* [String] Add a String module responsible for handling UTf-8 binaries
|
||||
|
||||
* Bug fixes
|
||||
* [File] `File.cp` and `File.cp_r` now preserves the file's mode
|
||||
* [IEx] Fix a bug where printing to `:stdio` on `IEx` was causing it to hang
|
||||
* [Macro] Fix a bug where quoted expressions were not behaving the same as their non-quoted counterparts
|
||||
* [Mix] `mix deps.get [DEPS]` now only gets the specified dependencies
|
||||
* [Mix] Mix now exits with status 1 in case of failures
|
||||
* [Protocol] Avoid false positives on protocol dispatch (a bug caused the dispatch to be triggered to an invalid protocol)
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [ExUnit] `setup` and `teardown` callbacks now receives the test name as second argument
|
||||
* [Kernel] Raw function definition with `def/4`, `defp/4`, `defmacro/4`, `defmacrop/4` now evaluates all arguments. The previous behaviour was accidental and did not properly evaluate all arguments
|
||||
* [Kernel] Change tuple-related (`elem` and `setelem`), Enum functions (`find_index`, `nth!` and `times`) and List functions (List.key*) to zero-index
|
||||
|
||||
* Deprecations
|
||||
* [Code] `Code.require_file` and `Code.load_file` now expect the full name as argument
|
||||
* [Enum] `List.reverse/1` and `List.zip/2` were moved to `Enum`
|
||||
* [GenServer] Rename `GenServer.Behavior` to `GenServer.Behaviour`
|
||||
* [Kernel] Bitstring syntax now uses `::` instead of `|`
|
||||
* [Kernel] `Erlang.` syntax is deprecated in favor of simply using atoms
|
||||
* [Module] `Module.read_attribute` and `Module.add_attribute` deprecated in favor of `Module.get_attribute` and `Module.put_attribute` which mimics Dict API
|
||||
|
||||
# v0.6.0 (2012-08-01)
|
||||
|
||||
* Backwards incompatible changes
|
||||
* [Kernel] Compile files now follow `Elixir-ModuleName` convention to solve issues with Erlang embedded mode. This removes the `__MAIN__` pseudo-variable as modules are now located inside `Elixir` namespace
|
||||
* [Kernel] `__using__` callback triggered by `use` now receives just one argument. Caller information can be accessed via macros using `__CALLER__`
|
||||
* [Kernel] Comprehensions syntax changed to be more compatible with Erlang behaviour
|
||||
* [Kernel] loop and recur are removed in favor of recursion with named functions
|
||||
* [Module] Removed data functions in favor of unifying the attributes API
|
||||
|
||||
* Deprecations
|
||||
* [Access] The semantics of the access protocol were reduced from a broad query API to simple data structure key-based access
|
||||
* [ExUnit] Some assertions are deprecated in favor of simply using `assert()`
|
||||
* [File] `File.read_info` is deprecated in favor of `File.stat`
|
||||
* [IO] `IO.print` is deprecated in favor of `IO.write`
|
||||
* [Kernel] Deprecate `__LINE__` and `__FUNCTION__` in favor of `__ENV__.line` and `__ENV__.function`
|
||||
* [Kernel] Deprecate `in_guard` in favor of `__CALLER__.in_guard?`
|
||||
* [Kernel] `refer` is deprecated in favor of `alias`
|
||||
* [Module] `Module.add_compile_callback(module, target, callback)` is deprecated in favor of `Module.put_attribute(module, :before_compile, {target, callback})`
|
||||
* [Module] `Module.function_defined?` is deprecated in favor of `Module.defines?`
|
||||
* [Module] `Module.defined_functions` is deprecated in favor of `Module.definitions_in`
|
||||
|
||||
* Enhancements
|
||||
* [Enum] Enhance Enum protocol to support `Enum.count`
|
||||
* [Enum] Optimize functions when a list is given as collection
|
||||
* [Enum] Add `find_index`, `nth!` and others
|
||||
* [ExUnit] Support setup and teardown callbacks
|
||||
* [IEx] IEx now provides autocomplete if the OS supports tty
|
||||
* [IEx] IEx now supports remsh
|
||||
* [IEx] Elixir now defaults to compile with documentation and `d` can be used in IEx to print modules and functions documentation
|
||||
* [IEx] Functions `c` and `m` are available in IEx to compile and print available module information. Functions `h` and `v` are available to show history and print previous commands values
|
||||
* [IO/File] Many improvements to `File` and `IO` modules
|
||||
* [Kernel] Operator `!` is now allowed in guard clauses
|
||||
* [Kernel] Introduce operator `=~` for regular expression matches
|
||||
* [Kernel] Compiled docs now include the function signature
|
||||
* [Kernel] `defmodule` do not start a new variable scope, this improves meta-programming capabilities
|
||||
* [Kernel] quote special form now supports line and unquote as options
|
||||
* [Kernel] Document the macro `@` and allow attributes to be read inside functions
|
||||
* [Kernel] Add support to the `%R` sigil. The same as `%r`, but without interpolation or escaping. Both implementations were also optimized to generate the regex at compilation time
|
||||
* [Kernel] Add `__ENV__` which returns a `Macro.Env` record with information about the compilation environment
|
||||
* [Kernel] Add `__CALLER__` inside macros which returns a `Macro.Env` record with information about the calling site
|
||||
* [Macro] Add `Macro.expand`, useful for debugging what a macro expands to
|
||||
* [Mix] First Mix public release
|
||||
* [Module] Add support to `@before_compile` and `@after_compile` callbacks. The first receives the module name while the latter receives the module name and its object code
|
||||
* [OptionParser] Make OptionParser public, add support to flags and improved switch parsing
|
||||
* [Range] Add a Range module with support to `in` operator (`x in 1..3`) and iterators
|
||||
* [Record] Allow `Record[_: value]` to set a default value to all records fields, as in Erlang
|
||||
* [Record] Records now provide a `to_keywords` function
|
||||
* [Regex] Back references are now properly supported
|
||||
* [System] Add `System.find_executable`
|
||||
|
||||
# v0.5.0 (2012-05-24)
|
||||
|
||||
* First official release
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
# Contributing to Elixir
|
||||
|
||||
Please take a moment to review this document in order to make the contribution
|
||||
process easy and effective for everyone involved!
|
||||
|
||||
## Using the issue tracker
|
||||
|
||||
Use the issues tracker for:
|
||||
|
||||
* [bug reports](#bugs-reports)
|
||||
* [submitting pull requests](#pull-requests)
|
||||
|
||||
Please **do not** use the issues tracker for personal support requests nor feature requests. Support requests should be send to:
|
||||
|
||||
* [the elixir-talk mailing list](http://groups.google.com/group/elixir-lang-talk)
|
||||
* [Stack Overflow](http://stackoverflow.com/questions/ask?tags=elixir)
|
||||
* [#elixir-lang](irc://chat.freenode.net/elixir-lang)
|
||||
|
||||
Feature requests can be discussed on [the elixir-core mailing list](http://groups.google.com/group/elixir-lang-core).
|
||||
|
||||
We do our best to keep the issues tracker tidy and organized, making it useful
|
||||
for everyone. For example, we classify open issues per application and perceived
|
||||
difficulty of the issue, making it easier for developers to
|
||||
[contribute to Elixir](#contributing).
|
||||
|
||||
## Bug reports
|
||||
|
||||
A bug is a _demonstrable problem_ that is caused by the code in the repository.
|
||||
Good bug reports are extremely helpful - thank you!
|
||||
|
||||
Guidelines for bug reports:
|
||||
|
||||
1. **Use the GitHub issue search** — check if the issue has already been
|
||||
reported.
|
||||
|
||||
2. **Check if the issue has been fixed** — try to reproduce it using the
|
||||
`master` branch in the repository.
|
||||
|
||||
3. **Isolate and report the problem** — ideally create a reduced test
|
||||
case.
|
||||
|
||||
Please try to be as detailed as possible in your report. Include information about
|
||||
your Operating System, your Erlang and Elixir versions. Please provide steps to
|
||||
reproduce the issue as well as the outcome you were expecting! All these details
|
||||
will help developers to fix any potential bugs.
|
||||
|
||||
Example:
|
||||
|
||||
> Short and descriptive example bug report title
|
||||
>
|
||||
> A summary of the issue and the environment in which it occurs. If suitable,
|
||||
> include the steps required to reproduce the bug.
|
||||
>
|
||||
> 1. This is the first step
|
||||
> 2. This is the second step
|
||||
> 3. Further steps, etc.
|
||||
>
|
||||
> `<url>` - a link to the reduced test case (e.g. a GitHub Gist)
|
||||
>
|
||||
> Any other information you want to share that is relevant to the issue being
|
||||
> reported. This might include the lines of code that you have identified as
|
||||
> causing the bug, and potential solutions (and your opinions on their
|
||||
> merits).
|
||||
|
||||
## Feature requests
|
||||
|
||||
Feature requests are welcome and should be discussed on [the elixir-core mailing list](http://groups.google.com/group/elixir-lang-core). But take a moment to find
|
||||
out whether your idea fits with the scope and aims of the project. It's up to *you*
|
||||
to make a strong case to convince the community of the merits of this feature.
|
||||
Please provide as much detail and context as possible.
|
||||
|
||||
## Contributing
|
||||
|
||||
We incentivate everyone to contribute to Elixir and help us tackle
|
||||
existing issues! To do so, there are a few things you need to know
|
||||
about the code. First, Elixir code is divided in applications inside
|
||||
the `lib` folder:
|
||||
|
||||
* `elixir` - Contains Elixir's kernel and stdlib
|
||||
|
||||
* `eex` - Template engine that allows you to embed Elixir
|
||||
|
||||
* `ex_unit` - Simple test framework that ships with Elixir
|
||||
|
||||
* `iex` — IEx, Elixir's interactive shell
|
||||
|
||||
* `mix` — Elixir's build tool
|
||||
|
||||
You can run all tests in the root directory with `make test` and you can
|
||||
also run tests for a specific framework `make test_#{NAME}`, for example,
|
||||
`make test_ex_unit`.
|
||||
|
||||
In case you are changing a single file, you can compile and run tests only
|
||||
for that particular file for fast development cycles. For example, if you
|
||||
are changing the String module, you can compile it and run its tests as:
|
||||
|
||||
$ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
|
||||
$ bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||
|
||||
After your changes are done, please remember to run the full suite with
|
||||
`make test`.
|
||||
|
||||
From time to time, your tests may fail in an existing Elixir checkout and
|
||||
may require a clean start by running `make clean compile`. You can always
|
||||
check [the official build status on Travis-CI](https://travis-ci.org/elixir-lang/elixir).
|
||||
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
send your pull requests.
|
||||
|
||||
## Contributing Documentation
|
||||
|
||||
Code documentation (`@doc`, `@moduledoc`, `@typedoc`) has a special convention:
|
||||
the first paragraph is considered to be a short summary.
|
||||
|
||||
For functions, macros and callbacks say what it will do. For example write
|
||||
something like:
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Returns only those elements for which `fun` is true.
|
||||
|
||||
...
|
||||
"""
|
||||
def filter(collection, fun) ...
|
||||
```
|
||||
|
||||
For modules, protocols and types say what it is. For example write
|
||||
something like:
|
||||
|
||||
```elixir
|
||||
defmodule File.Stat do
|
||||
@moduledoc """
|
||||
Information about a file.
|
||||
|
||||
...
|
||||
"""
|
||||
|
||||
defstruct [...]
|
||||
end
|
||||
```
|
||||
|
||||
Keep in mind that the first paragraph might show up in a summary somewhere, long
|
||||
texts in the first paragraph create very ugly summaries. As a rule of thumb
|
||||
anything longer than 80 characters is too long.
|
||||
|
||||
Try to keep unneccesary details out of the first paragraph, it's only there to
|
||||
give a user a quick idea of what the documented "thing" does/is. The rest of the
|
||||
documentation string can contain the details, for example when a value and when
|
||||
`nil` is returned.
|
||||
|
||||
If possible include examples, preferably in a form that works with doctests. For
|
||||
example:
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Return only those elements for which `fun` is true.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.filter([1, 2, 3], fn(x) -> rem(x, 2) == 0 end)
|
||||
[2]
|
||||
|
||||
"""
|
||||
def filter(collection, fun) ...
|
||||
```
|
||||
|
||||
This makes it easy to test the examples so that they don't go stale and examples
|
||||
are often a great help in explaining what a function does.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Good pull requests - patches, improvements, new features - are a fantastic
|
||||
help. They should remain focused in scope and avoid containing unrelated
|
||||
commits.
|
||||
|
||||
**IMPORTANT**: By submitting a patch, you agree that your work will be
|
||||
licensed under the license used by the project.
|
||||
|
||||
If you have any large pull request in mind (e.g. implementing features,
|
||||
refactoring code, etc), **please ask first** otherwise you risk spending
|
||||
a lot of time working on something that the project's developers might
|
||||
not want to merge into the project.
|
||||
|
||||
Please adhere to the coding conventions in the project (indentation,
|
||||
accurate comments, etc.) and don't forget to add your own tests and
|
||||
documentation. When working with git, we recommend the following process
|
||||
in order to craft an excellent pull request:
|
||||
|
||||
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
|
||||
and configure the remotes:
|
||||
|
||||
```bash
|
||||
# Clone your fork of the repo into the current directory
|
||||
git clone https://github.com/<your-username>/elixir
|
||||
# Navigate to the newly cloned directory
|
||||
cd elixir
|
||||
# Assign the original repo to a remote called "upstream"
|
||||
git remote add upstream https://github.com/elixir-lang/elixir
|
||||
```
|
||||
|
||||
2. If you cloned a while ago, get the latest changes from upstream:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
```
|
||||
|
||||
3. Create a new topic branch (off of `master`) to contain your feature, change,
|
||||
or fix.
|
||||
|
||||
**IMPORTANT**: Making changes in `master` is discouraged. You should always
|
||||
keep your local `master` in sync with upstream `master` and make your
|
||||
changes in topic branches.
|
||||
|
||||
```bash
|
||||
git checkout -b <topic-branch-name>
|
||||
```
|
||||
|
||||
4. Commit your changes in logical chunks. Keep your commit messages organized,
|
||||
with a short description in the first line and more detailed information on
|
||||
the following lines. Feel free to use Git's
|
||||
[interactive rebase](https://help.github.com/articles/interactive-rebase)
|
||||
feature to tidy up your commits before making them public.
|
||||
|
||||
5. Make sure all the tests are still passing.
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
This command will compile the code in your branch and use that
|
||||
version of Elixir to run the tests. This is needed to ensure your changes can
|
||||
pass all the tests.
|
||||
|
||||
6. Push your topic branch up to your fork:
|
||||
|
||||
```bash
|
||||
git push origin <topic-branch-name>
|
||||
```
|
||||
|
||||
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
|
||||
with a clear title and description.
|
||||
|
||||
8. If you haven't updated your pull request for a while, you should consider
|
||||
rebasing on master and resolving any conflicts.
|
||||
|
||||
**IMPORTANT**: _Never ever_ merge upstream `master` into your branches. You
|
||||
should always `git rebase` on `master` to bring your changes up to date when
|
||||
necessary.
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
git checkout <your-topic-branch>
|
||||
git rebase master
|
||||
```
|
||||
|
||||
We have saved some excellent pull requests we have received in the past in case
|
||||
you are looking for some examples:
|
||||
|
||||
* https://github.com/elixir-lang/elixir/pull/992
|
||||
* https://github.com/elixir-lang/elixir/pull/1041
|
||||
* https://github.com/elixir-lang/elixir/pull/1058
|
||||
* https://github.com/elixir-lang/elixir/pull/1059
|
||||
|
||||
Thank you for your contributions!
|
||||
@@ -1,8 +0,0 @@
|
||||
LEGAL NOTICE INFORMATION
|
||||
------------------------
|
||||
|
||||
All the files in this distribution are covered under either Elixir's
|
||||
license (see the file LICENSE) except the files mentioned below that
|
||||
contains sections that are under Erlang's License (EPL):
|
||||
|
||||
lib/elixir/src/elixir_parser.erl (generated by build scripts)
|
||||
@@ -1,13 +0,0 @@
|
||||
Copyright 2012-2013 Plataformatec.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,179 +0,0 @@
|
||||
REBAR := rebar
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
VERSION := $(strip $(shell cat VERSION))
|
||||
Q := @
|
||||
PREFIX := /usr/local
|
||||
LIBDIR := lib
|
||||
INSTALL = install
|
||||
INSTALL_DIR = $(INSTALL) -m755 -d
|
||||
INSTALL_DATA = $(INSTALL) -m644
|
||||
INSTALL_PROGRAM = $(INSTALL) -m755
|
||||
|
||||
.PHONY: install compile erlang elixir dialyze test clean docs release_docs release_zip check_erlang_release
|
||||
.NOTPARALLEL: compile
|
||||
|
||||
#==> Functions
|
||||
|
||||
# This check should work for older versions like R16B
|
||||
# as well as new verions like 17.1 and 18
|
||||
define CHECK_ERLANG_RELEASE
|
||||
$(Q) erl -noshell -eval 'io:fwrite("~s", [erlang:system_info(otp_release)])' -s erlang halt | grep -q '^1[789]'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang 17.0 is required to build Elixir"; \
|
||||
exit 1; \
|
||||
fi;
|
||||
endef
|
||||
|
||||
define APP_TEMPLATE
|
||||
$(1): lib/$(1)/ebin/Elixir.$(2).beam lib/$(1)/ebin/$(1).app
|
||||
|
||||
lib/$(1)/ebin/$(1).app: lib/$(1)/mix.exs
|
||||
$(Q) mkdir -p lib/$(1)/_build/shared/lib/$(1)
|
||||
$(Q) cp -R lib/$(1)/ebin lib/$(1)/_build/shared/lib/$(1)/
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -e "Mix.Sup.start_link()" -r mix.exs -e "Mix.Task.run('compile.app')"
|
||||
$(Q) cp lib/$(1)/_build/shared/lib/$(1)/ebin/$(1).app lib/$(1)/ebin/$(1).app
|
||||
$(Q) rm -rf lib/$(1)/_build
|
||||
|
||||
lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1)/lib/*/*.ex) $(wildcard lib/$(1)/lib/*/*/*.ex)
|
||||
@ echo "==> $(1) (compile)"
|
||||
@ rm -rf lib/$(1)/ebin
|
||||
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
|
||||
|
||||
test_$(1): $(1)
|
||||
@ echo "==> $(1) (exunit)"
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/*_test.exs";
|
||||
endef
|
||||
|
||||
#==> Compilation tasks
|
||||
|
||||
KERNEL:=lib/elixir/ebin/Elixir.Kernel.beam
|
||||
UNICODE:=lib/elixir/ebin/Elixir.String.Unicode.beam
|
||||
|
||||
default: compile
|
||||
|
||||
compile: lib/elixir/src/elixir.app.src erlang elixir
|
||||
|
||||
lib/elixir/src/elixir.app.src: src/elixir.app.src
|
||||
$(Q) $(call CHECK_ERLANG_RELEASE)
|
||||
$(Q) rm -rf lib/elixir/src/elixir.app.src
|
||||
$(Q) echo "%% This file is automatically generated from <project_root>/src/elixir.app.src" \
|
||||
>lib/elixir/src/elixir.app.src
|
||||
$(Q) cat src/elixir.app.src >>lib/elixir/src/elixir.app.src
|
||||
|
||||
erlang:
|
||||
$(Q) cd lib/elixir && ../../$(REBAR) compile
|
||||
|
||||
# Since Mix depends on EEx and EEx depends on
|
||||
# Mix, we first compile EEx without the .app
|
||||
# file, then mix and then compile eex fully
|
||||
elixir: kernel lib/eex/ebin/Elixir.EEx.beam mix ex_unit eex iex
|
||||
|
||||
kernel: $(KERNEL) VERSION
|
||||
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex
|
||||
$(Q) if [ ! -f $(KERNEL) ]; then \
|
||||
echo "==> bootstrap (compile)"; \
|
||||
$(ERL) -s elixir_compiler core -s erlang halt; \
|
||||
fi
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/kernel.ex" -o ebin;
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(MAKE) unicode
|
||||
$(Q) rm -rf lib/elixir/ebin/elixir.app
|
||||
$(Q) cd lib/elixir && ../../$(REBAR) compile
|
||||
|
||||
unicode: $(UNICODE)
|
||||
$(UNICODE): lib/elixir/unicode/*
|
||||
@ echo "==> unicode (compile)";
|
||||
@ echo "This step can take up to a minute to compile in order to embed the Unicode database"
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) unicode/unicode.ex -o ebin;
|
||||
|
||||
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
|
||||
$(eval $(call APP_TEMPLATE,eex,EEx))
|
||||
$(eval $(call APP_TEMPLATE,mix,Mix))
|
||||
$(eval $(call APP_TEMPLATE,iex,IEx))
|
||||
|
||||
install: compile
|
||||
@ echo "==> elixir (install)"
|
||||
$(Q) for dir in lib/*; do \
|
||||
$(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
|
||||
$(INSTALL_DATA) $$dir/ebin/* "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
|
||||
done
|
||||
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
|
||||
$(Q) $(INSTALL_PROGRAM) $(filter-out %.bat, $(wildcard bin/*)) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
|
||||
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/bin"
|
||||
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/* ; do \
|
||||
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/bin/" ; \
|
||||
done
|
||||
|
||||
clean:
|
||||
cd lib/elixir && ../../$(REBAR) clean
|
||||
rm -rf ebin
|
||||
rm -rf lib/*/ebin
|
||||
rm -rf lib/elixir/test/ebin
|
||||
rm -rf lib/*/tmp
|
||||
rm -rf lib/mix/test/fixtures/git_repo
|
||||
rm -rf lib/mix/test/fixtures/deps_on_git_repo
|
||||
rm -rf lib/mix/test/fixtures/git_rebar
|
||||
rm -rf lib/elixir/src/elixir.app.src
|
||||
|
||||
clean_exbeam:
|
||||
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
||||
|
||||
#==> Release tasks
|
||||
|
||||
SOURCE_REF = $(shell head="$$(git rev-parse HEAD)" tag="$$(git tag --points-at $$head | tail -1)" ; echo "$${tag:-$$head}\c")
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc
|
||||
mkdir -p ebin
|
||||
rm -rf docs
|
||||
cp -R -f lib/*/ebin/*.beam ./ebin
|
||||
bin/elixir ../ex_doc/bin/ex_doc "Elixir" "$(VERSION)" "./ebin" -m Kernel -u "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)"
|
||||
rm -rf ebin
|
||||
|
||||
../ex_doc/bin/ex_doc:
|
||||
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
|
||||
@ false
|
||||
|
||||
release_zip: compile
|
||||
rm -rf v$(VERSION).zip
|
||||
zip -9 -r v$(VERSION).zip bin CHANGELOG.md LEGAL lib/*/ebin LICENSE README.md VERSION
|
||||
|
||||
release_docs: docs
|
||||
cd ../docs
|
||||
rm -rf ../docs/master
|
||||
mv docs ../docs/master
|
||||
|
||||
#==> Tests tasks
|
||||
|
||||
test: test_erlang test_elixir
|
||||
|
||||
test_erlang: compile
|
||||
@ echo "==> elixir (eunit)"
|
||||
$(Q) mkdir -p lib/elixir/test/ebin
|
||||
$(Q) $(ERLC) -pa lib/elixir/ebin -o lib/elixir/test/ebin lib/elixir/test/erlang/*.erl
|
||||
$(Q) $(ERL) -pa lib/elixir/test/ebin -s test_helper test -s erlang halt;
|
||||
@ echo ""
|
||||
|
||||
test_elixir: test_kernel test_ex_unit test_doc_test test_mix test_eex test_iex
|
||||
|
||||
test_doc_test: compile
|
||||
@ echo "==> doctest (exunit)"
|
||||
$(Q) cd lib/elixir && ../../bin/elixir -r "test/doc_test.exs";
|
||||
|
||||
test_kernel: compile
|
||||
@ echo "==> elixir (exunit)"
|
||||
$(Q) cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs";
|
||||
|
||||
.dialyzer.base_plt:
|
||||
@ echo "==> Adding Erlang/OTP basic applications to a new base PLT"
|
||||
$(Q) dialyzer --output_plt .dialyzer.base_plt --build_plt --apps erts kernel stdlib compiler tools syntax_tools parsetools
|
||||
|
||||
dialyze: .dialyzer.base_plt
|
||||
$(Q) rm -f .dialyzer_plt
|
||||
$(Q) cp .dialyzer.base_plt .dialyzer_plt
|
||||
@ echo "==> Adding Elixir to PLT..."
|
||||
$(Q) dialyzer --plt .dialyzer_plt --add_to_plt -r lib/elixir/ebin lib/ex_unit/ebin lib/eex/ebin lib/iex/ebin lib/mix/ebin
|
||||
@ echo "==> Dialyzing Elixir..."
|
||||
$(Q) dialyzer --plt .dialyzer_plt -r lib/elixir/ebin lib/ex_unit/ebin lib/eex/ebin lib/iex/ebin lib/mix/ebin
|
||||
@@ -1,57 +1,94 @@
|
||||

|
||||
=========
|
||||
[](http://travis-ci.org/elixir-lang/elixir)
|
||||
# n8n-openai-adapter
|
||||
|
||||
For more about Elixir, installation and documentation, [check Elixir's website](http://elixir-lang.org/).
|
||||
An OpenAI-compatible HTTP adapter that exposes self-hosted **n8n chat agents**
|
||||
behind a standard `/v1/chat/completions` API, so any OpenAI client (Cursor,
|
||||
LibreChat, the `openai` SDK, a custom app) can talk to your n8n agents as if
|
||||
they were OpenAI models.
|
||||
|
||||
## Usage
|
||||
n8n itself does **not** ship an inbound OpenAI-compatible endpoint (its "AI
|
||||
Gateway" is an outbound proxy to n8n Cloud). This small Elixir service is the
|
||||
bridge: one `/v1/chat/completions` endpoint, routed to whichever n8n agent you
|
||||
name in the `model` field.
|
||||
|
||||
If you want to contribute to Elixir or run it from source, clone this repository to your machine, compile and test it:
|
||||
## How it works
|
||||
|
||||
$ git clone https://github.com/elixir-lang/elixir.git
|
||||
$ cd elixir
|
||||
$ make clean test
|
||||
```
|
||||
Your OpenAI client
|
||||
POST /v1/chat/completions {"model":"scholar-agent","thread_id":"abc","messages":[...]}
|
||||
|
|
||||
v
|
||||
n8n-openai-adapter (Plug + Bandit)
|
||||
- authorize (Bearer <ADAPTER_API_KEY>)
|
||||
- look up "scholar-agent" -> n8n chat webhook URL (AgentRegistry GenServer)
|
||||
- take the last user message
|
||||
- forward to the n8n webhook {sessionId: thread_id, action: sendMessage, chatInput}
|
||||
|
|
||||
v
|
||||
n8n agent (its MCP tools, memory, etc. run as usual)
|
||||
|
|
||||
v
|
||||
returns OpenAI-shaped {"choices":[{"message":{"role":"assistant","content":...}}]}
|
||||
```
|
||||
|
||||
If Elixir fails to build (specifically when pulling in a new version via git), be sure to remove any previous build artifacts by running `make clean`, then `make test`.
|
||||
Multiple agents = multiple `model` names, each mapped to a different n8n webhook
|
||||
in the `AGENTS` env var.
|
||||
|
||||
If tests pass, you are ready to move on to the [Getting Started guide][1] or to try Interactive Elixir by running: `bin/iex` in your terminal.
|
||||
## Configuration (env vars)
|
||||
|
||||
However, if tests fail, it is likely you have an outdated Erlang version (Elixir requires Erlang 17.0 or later). You can check your Erlang version by calling `erl` in the command line. You will see some information as follows:
|
||||
| Var | Required | Purpose |
|
||||
|------------------|----------|---------------------------------------------------------------------|
|
||||
| `ADAPTER_API_KEY`| yes | Bearer key that OpenAI clients send. |
|
||||
| `ADMIN_API_KEY` | yes | Bearer key for the admin API / web admin page. |
|
||||
| `AGENTS_FILE` | no | Path to the JSON store (default `/var/lib/n8n-openai/agents.json`). |
|
||||
| `PORT` | no | HTTP port (default `8000`). |
|
||||
| `CHAT_WEBHOOK_BASIC` | no | `"user:password"` if your n8n Chat Trigger is Basic-auth protected. |
|
||||
|
||||
Erlang/OTP 17 [erts-6.0] [source-07b8f44] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
|
||||
Agents are **not** configured via env — they're managed at runtime through the
|
||||
web admin page / admin API and persisted to `AGENTS_FILE`. The store starts
|
||||
empty; add agents after boot.
|
||||
|
||||
If you have the correct version and tests still fail, feel free to [open an issue][2].
|
||||
## Admin API (manage agents at runtime)
|
||||
|
||||
## Building documentation
|
||||
Agents are persisted to `AGENTS_FILE` and can be added/removed without a
|
||||
redeploy, using the `ADMIN_API_KEY`:
|
||||
|
||||
Building the documentation requires [ex_doc](https://github.com/elixir-lang/ex_doc) to be installed and built in the same containing folder as elixir.
|
||||
```bash
|
||||
# list
|
||||
curl -H "Authorization: Bearer $ADMIN_API_KEY" https://openai.bueso.eu/admin/agents
|
||||
|
||||
# After cloning and compiling Elixir
|
||||
$ git clone git://github.com/elixir-lang/ex_doc.git
|
||||
$ cd ex_doc && ../elixir/bin/mix compile
|
||||
$ cd ../elixir && make docs
|
||||
# add / update an agent
|
||||
curl -X POST -H "Authorization: Bearer $ADMIN_API_KEY" -H "Content-Type: application/json" \
|
||||
-d '{"model":"media-agent","webhook":"https://n8n.bueso.eu/webhook/<id>/chat"}' \
|
||||
https://openai.bueso.eu/admin/agents
|
||||
|
||||
## Contributing
|
||||
# remove
|
||||
curl -X DELETE -H "Authorization: Bearer $ADMIN_API_KEY" \
|
||||
https://openai.bueso.eu/admin/agents/media-agent
|
||||
```
|
||||
|
||||
We appreciate any contribution to Elixir, so check out our [CONTRIBUTING.md](CONTRIBUTING.md) guide for more information. We usually keep a list of features and bugs [in the issue tracker][2].
|
||||
The store is authoritative and persists across restarts; no env config needed.
|
||||
|
||||
## Important links
|
||||
## Building & running
|
||||
|
||||
* #elixir-lang on freenode IRC
|
||||
* [Website][1]
|
||||
* [Issue tracker][2]
|
||||
* [elixir-talk Mailing list (questions)][3]
|
||||
* [elixir-core Mailing list (development)][4]
|
||||
```bash
|
||||
mix deps.get
|
||||
mix compile
|
||||
ADAPTER_API_KEY=secret AGENTS='{"scholar-agent":"https://n8n.bueso.eu/webhook/<id>/chat"}' \
|
||||
PORT=8000 mix run --no-halt
|
||||
```
|
||||
|
||||
[1]: http://elixir-lang.org
|
||||
[2]: https://github.com/elixir-lang/elixir/issues
|
||||
[3]: http://groups.google.com/group/elixir-lang-talk
|
||||
[4]: http://groups.google.com/group/elixir-lang-core
|
||||
## Testing
|
||||
|
||||
## License
|
||||
```bash
|
||||
MIX_ENV=test mix test
|
||||
```
|
||||
|
||||
"Elixir" and the Elixir logo are copyright (c) 2012 Plataformatec.
|
||||
## Nix
|
||||
|
||||
Elixir source code is released under Apache 2 License with some parts under Erlang's license (EPL).
|
||||
The repo ships a `flake.nix` exporting `overlays.default` and a `packages.default`
|
||||
(the packaged BEAM release), so it can be consumed as a flake input from your
|
||||
NixOS config just like any other flake — e.g.:
|
||||
|
||||
Check LEGAL and LICENSE files for more information.
|
||||
```nix
|
||||
inputs.n8n-openai-adapter.url = "git+https://gitea.bueso.eu/<owner>/n8n-openai-adapter";
|
||||
```
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
## Release process
|
||||
|
||||
This document simply outlines the release process:
|
||||
|
||||
1) Remove `-dev` extension from VERSION
|
||||
|
||||
2) Ensure CHANGELOG is updated and timestamp
|
||||
|
||||
3) Commit changes above with title "Release vVERSION" and generate new tag
|
||||
|
||||
4) Run `make clean test` to ensure all tests pass from scratch and the CI is green
|
||||
|
||||
5) Push master and tags
|
||||
|
||||
6) Release new docs with `make release_docs`, move docs to `docs/stable`
|
||||
|
||||
7) Release new zip with `make release_zip`, push new zip to GitHub Releases
|
||||
|
||||
8) Merge master into stable branch and push it
|
||||
|
||||
9) After release, bump versions, add `-dev` back and commit
|
||||
|
||||
10) `make release_docs` once again and push it to `elixir-lang.github.com`
|
||||
|
||||
## Places where version is mentioned
|
||||
|
||||
* VERSION
|
||||
* CHANGELOG
|
||||
* src/elixir.app.src
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: `basename $0` [options] [.exs file] [data]
|
||||
|
||||
-v Prints version and exit
|
||||
-e \"command\" Evaluates the given command (*)
|
||||
-r \"file\" Requires the given files/patterns (*)
|
||||
-S \"script\" Finds and executes the given script
|
||||
-pr \"file\" Requires the given files/patterns in parallel (*)
|
||||
-pa \"path\" Prepends the given path to Erlang code path (*)
|
||||
-pz \"path\" Appends the given path to Erlang code path (*)
|
||||
--app \"app\" Start the given app and its dependencies (*)
|
||||
--erl \"switches\" Switches to be passed down to erlang (*)
|
||||
--name \"name\" Makes and assigns a name to the distributed node
|
||||
--sname \"name\" Makes and assigns a short name to the distributed node
|
||||
--cookie \"cookie\" Sets a cookie for this distributed node
|
||||
--hidden Makes a hidden node
|
||||
--detached Starts the Erlang VM detached from console
|
||||
--no-halt Does not halt the Erlang VM after execution
|
||||
--gen-debug Turns on default debugging for all GenServers
|
||||
|
||||
** Options marked with (*) can be given more than once
|
||||
** Options given after the .exs file or -- are passed down to the executed code
|
||||
** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS or --erl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd "$(dirname "$1")" > /dev/null
|
||||
filename="$(basename "$1")"
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f "$(readlink "$filename")"
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
MODE="elixir"
|
||||
ERL=""
|
||||
I=1
|
||||
|
||||
while [ $I -le $# ]; do
|
||||
S=1
|
||||
eval "PEEK=\${$I}"
|
||||
case "$PEEK" in
|
||||
+iex)
|
||||
MODE="iex"
|
||||
;;
|
||||
+elixirc)
|
||||
MODE="elixirc"
|
||||
;;
|
||||
-v|--compile|--no-halt)
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--remsh|--app)
|
||||
S=2
|
||||
;;
|
||||
--detached|--hidden)
|
||||
ERL="$ERL `echo $PEEK | cut -c 2-`"
|
||||
;;
|
||||
--cookie)
|
||||
I=$(expr $I + 1)
|
||||
eval "VAL=\${$I}"
|
||||
ERL="$ERL -setcookie "$VAL""
|
||||
;;
|
||||
--sname|--name)
|
||||
I=$(expr $I + 1)
|
||||
eval "VAL=\${$I}"
|
||||
ERL="$ERL `echo $PEEK | cut -c 2-` "$VAL""
|
||||
;;
|
||||
--erl)
|
||||
I=$(expr $I + 1)
|
||||
eval "VAL=\${$I}"
|
||||
ERL="$ERL "$VAL""
|
||||
;;
|
||||
--gen-debug)
|
||||
ERL="$ERL -generic_debug"
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
I=$(expr $I + $S)
|
||||
done
|
||||
|
||||
SELF=$(readlink_f "$0")
|
||||
SCRIPT_PATH=$(dirname "$SELF")
|
||||
if [ "$MODE" != "iex" ]; then ERL="$ERL -s elixir start_cli"; fi
|
||||
|
||||
if [ -z "$ERL_PATH" ]; then
|
||||
if [ -f "$SCRIPT_PATH/../releases/RELEASES" ] && [ -f "$SCRIPT_PATH/erl" ]; then
|
||||
ERL_PATH="$SCRIPT_PATH"/erl
|
||||
else
|
||||
ERL_PATH=erl
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$ERL_PATH" -pa "$SCRIPT_PATH"/../lib/*/ebin -noshell $ELIXIR_ERL_OPTS $ERL -extra "$@"
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
@echo off
|
||||
set argc=0
|
||||
for %%A in (%*) do (
|
||||
if "%%A"=="--help" goto documentation
|
||||
if "%%A"=="-h" goto documentation
|
||||
if "%%A"=="/h" goto documentation
|
||||
set /A argc+=1
|
||||
)
|
||||
if %argc%==0 goto documentation
|
||||
goto parseopts
|
||||
|
||||
:documentation
|
||||
echo Usage: %~nx0 [options] [.exs file] [data]
|
||||
echo.
|
||||
echo -v Prints version and exit
|
||||
echo -e command Evaluates the given command (*)
|
||||
echo -r file Requires the given files/patterns (*)
|
||||
echo -S script Finds and executes the given script
|
||||
echo -pr file Requires the given files/patterns in parallel (*)
|
||||
echo -pa path Prepends the given path to Erlang code path (*)
|
||||
echo -pz path Appends the given path to Erlang code path (*)
|
||||
echo --app app Start the given app and its dependencies (*)
|
||||
echo --erl switches Switches to be passed down to erlang (*)
|
||||
echo --name name Makes and assigns a name to the distributed node
|
||||
echo --sname name Makes and assigns a short name to the distributed node
|
||||
echo --cookie cookie Sets a cookie for this distributed node
|
||||
echo --hidden Makes a hidden node
|
||||
echo --detached Starts the Erlang VM detached from console
|
||||
echo --no-halt Does not halt the Erlang VM after execution
|
||||
echo --gen-debug Turns on default debugging for all GenServers
|
||||
echo.
|
||||
echo ** Options marked with (*) can be given more than once
|
||||
echo ** Options given after the .exs file or -- are passed down to the executed code
|
||||
echo ** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS or --erl
|
||||
goto :EOF
|
||||
|
||||
:parseopts
|
||||
|
||||
rem Parameters for Erlang
|
||||
set parsErlang=
|
||||
|
||||
rem Make sure we keep a copy of all parameters
|
||||
set allPars=%*
|
||||
|
||||
rem Get the original path name from the batch file
|
||||
set originPath=%~dp0
|
||||
|
||||
rem Optional parameters before the "-extra" parameter
|
||||
set beforeExtra=
|
||||
|
||||
rem Flag which determines whether or not to use werl vs erl
|
||||
set useWerl=0
|
||||
|
||||
rem Recursive loop called for each parameter that parses the cmd line parameters
|
||||
:startloop
|
||||
set par="%1"
|
||||
shift
|
||||
if "%par%"=="" (
|
||||
rem if no parameters defined
|
||||
goto :expand_erl_libs
|
||||
)
|
||||
if "%par%"=="""" (
|
||||
rem if no parameters defined - special case for parameter that is already quoted
|
||||
goto :expand_erl_libs
|
||||
)
|
||||
rem ******* EXECUTION OPTIONS **********************
|
||||
IF "%par%"==""+iex"" (Set useWerl=1)
|
||||
rem ******* ERLANG PARAMETERS **********************
|
||||
IF NOT "%par%"=="%par:--detached=%" (Set parsErlang=%parsErlang% -detached)
|
||||
IF NOT "%par%"=="%par:--hidden=%" (Set parsErlang=%parsErlang% -hidden)
|
||||
IF NOT "%par%"=="%par:--cookie=%" (Set parsErlang=%parsErlang% -setcookie %1 && shift)
|
||||
IF NOT "%par%"=="%par:--sname=%" (Set parsErlang=%parsErlang% -sname %1 && shift)
|
||||
IF NOT "%par%"=="%par:--name=%" (Set parsErlang=%parsErlang% -name %1 && shift)
|
||||
IF NOT "%par%"=="%par:--erl=%" (Set beforeExtra=%beforeExtra% %~1 && shift)
|
||||
IF NOT "%par%"=="%par:--gen-debug=%" (Set parsErlang=%parsErlang% -generic_debug)
|
||||
rem ******* elixir parameters **********************
|
||||
rem Note: we don't have to do anything with options that don't take an argument
|
||||
IF NOT "%par%"=="%par:-e=%" (shift)
|
||||
IF NOT "%par%"=="%par:-r=%" (shift)
|
||||
IF NOT "%par%"=="%par:-pr=%" (shift)
|
||||
IF NOT "%par%"=="%par:-pa=%" (shift)
|
||||
IF NOT "%par%"=="%par:-pz=%" (shift)
|
||||
IF NOT "%par%"=="%par:--app=%" (shift)
|
||||
IF NOT "%par%"=="%par:--remsh=%" (shift)
|
||||
goto:startloop
|
||||
|
||||
rem ******* assume all pre-params are parsed ********************
|
||||
:expand_erl_libs
|
||||
rem ******* expand all ebin paths as Windows does not support the ..\*\ebin wildcard ********************
|
||||
SETLOCAL enabledelayedexpansion
|
||||
set ext_libs=
|
||||
for /d %%d in ("%originPath%..\lib\*.") do (
|
||||
set ext_libs=!ext_libs! -pa "%%~fd\ebin"
|
||||
)
|
||||
SETLOCAL disabledelayedexpansion
|
||||
:run
|
||||
IF %useWerl% EQU 1 (
|
||||
werl %ext_libs% -noshell %ELIXIR_ERL_OPTS% %parsErlang% -s elixir start_cli %beforeExtra% -extra %*
|
||||
) ELSE (
|
||||
erl %ext_libs% -noshell %ELIXIR_ERL_OPTS% %parsErlang% -s elixir start_cli %beforeExtra% -extra %*
|
||||
)
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: `basename $0` [elixir switches] [compiler switches] [.ex files]
|
||||
|
||||
-o The directory to output compiled files
|
||||
--no-docs Do not attach documentation to compiled modules
|
||||
--no-debug-info Do not attach debug info to compiled modules
|
||||
--ignore-module-conflict
|
||||
--warnings-as-errors Treat warnings as errors and return non-zero exit code
|
||||
--verbose Print informational messages.
|
||||
|
||||
** Options given after -- are passed down to the executed code
|
||||
** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd "$(dirname "$1")" > /dev/null
|
||||
filename="$(basename "$1")"
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f "$(readlink "$filename")"
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=$(readlink_f "$0")
|
||||
SCRIPT_PATH=$(dirname "$SELF")
|
||||
exec "$SCRIPT_PATH"/elixir +elixirc "$@"
|
||||
@@ -1,26 +0,0 @@
|
||||
@echo off
|
||||
set argc=0
|
||||
for %%A in (%*) do (
|
||||
if "%%A"=="--help" goto documentation
|
||||
if "%%A"=="-h" goto documentation
|
||||
if "%%A"=="/h" goto documentation
|
||||
set /A argc+=1
|
||||
)
|
||||
if %argc%==0 goto documentation
|
||||
goto run
|
||||
|
||||
:documentation
|
||||
echo Usage: %~nx0 [elixir switches] [compiler switches] [.ex files]
|
||||
echo.
|
||||
echo -o The directory to output compiled files
|
||||
echo --no-docs Do not attach documentation to compiled modules
|
||||
echo --no-debug-info Do not attach debug info to compiled modules
|
||||
echo --ignore-module-conflict
|
||||
echo --warnings-as-errors Treat warnings as errors and return non-zero exit code
|
||||
echo --verbose Print informational messages.
|
||||
echo.
|
||||
echo ** Options marked with (*) can be given more than once
|
||||
echo ** Options given after -- are passed down to the executed code
|
||||
echo ** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS" >&2
|
||||
:run
|
||||
call "%~dp0\elixir.bat" +elixirc %*
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -gt 0 ] && ([ "$1" = "--help" ] || [ "$1" = "-h" ]); then
|
||||
echo "Usage: `basename $0` [options] [.exs file] [data]
|
||||
|
||||
-v Prints version
|
||||
-e \"command\" Evaluates the given command (*)
|
||||
-r \"file\" Requires the given files/patterns (*)
|
||||
-S \"script\" Finds and executes the given script
|
||||
-pr \"file\" Requires the given files/patterns in parallel (*)
|
||||
-pa \"path\" Prepends the given path to Erlang code path (*)
|
||||
-pz \"path\" Appends the given path to Erlang code path (*)
|
||||
--app \"app\" Start the given app and its dependencies (*)
|
||||
--erl \"switches\" Switches to be passed down to erlang (*)
|
||||
--name \"name\" Makes and assigns a name to the distributed node
|
||||
--sname \"name\" Makes and assigns a short name to the distributed node
|
||||
--cookie \"cookie\" Sets a cookie for this distributed node
|
||||
--hidden Makes a hidden node
|
||||
--detached Starts the Erlang VM detached from console
|
||||
--gen-debug Turns on default debugging for all GenServers
|
||||
--remsh \"name\" Connects to a node using a remote shell
|
||||
--dot-iex \"path\" Overrides default .iex.exs file and uses path instead;
|
||||
path can be empty, then no file will be loaded
|
||||
|
||||
** Options marked with (*) can be given more than once
|
||||
** Options given after the .exs file or -- are passed down to the executed code
|
||||
** Options can be passed to the VM using ELIXIR_ERL_OPTS or --erl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd "$(dirname "$1")" > /dev/null
|
||||
filename="$(basename "$1")"
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f "$(readlink "$filename")"
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=$(readlink_f "$0")
|
||||
SCRIPT_PATH=$(dirname "$SELF")
|
||||
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-user Elixir.IEx.CLI" +iex "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
call "%~dp0\elixir.bat" +iex --erl "-user Elixir.IEx.CLI" --no-halt %*
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
call "%~dp0\elixir.bat" "%~dp0\mix" %*
|
||||
@@ -0,0 +1,7 @@
|
||||
import Config
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
if config_env() == :test do
|
||||
config :logger, level: :warning
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
import Config
|
||||
|
||||
# Dev: no special config — all runtime settings come from env vars.
|
||||
@@ -0,0 +1,5 @@
|
||||
import Config
|
||||
|
||||
# Production: no hardcoded values here. All runtime config (PORT, AGENTS,
|
||||
# ADAPTER_API_KEY, CHAT_WEBHOOK_BASIC) comes from the systemd EnvironmentFile
|
||||
# in the NixOS service module.
|
||||
@@ -0,0 +1,7 @@
|
||||
import Config
|
||||
|
||||
# Test environment: the app starts with an empty agent store (no AGENTS env
|
||||
# seeding — agents are managed via the admin API). ADAPTER_API_KEY /
|
||||
# ADMIN_API_KEY are set in test/test_helper.exs. AGENTS_FILE must be set HERE
|
||||
# (config loads before the app boots) to a writable tmp path.
|
||||
System.put_env("AGENTS_FILE", Path.join(System.tmp_dir!(), "n8n-openai-test-agents.json"))
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1788881743,
|
||||
"narHash": "sha256-2V9GZGvPfrNzxFozhI9dcqV+c3QdA8YZrvAAzqEB+dI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
description = "OpenAI-compatible adapter exposing n8n chat agents behind /v1/chat/completions";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs, ... }:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
beamPackages = pkgs.beamPackages;
|
||||
in
|
||||
{
|
||||
default = beamPackages.mixRelease {
|
||||
pname = "n8n-openai-adapter";
|
||||
version = "0.1.0";
|
||||
src = self;
|
||||
mixFodDeps = beamPackages.fetchMixDeps {
|
||||
pname = "n8n-openai-adapter";
|
||||
version = "0.1.0";
|
||||
src = self;
|
||||
hash = "sha256-sdAhpZUeF33V9xjEa/z/aTmCllfetMjO/1XyfJfUNao=";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: prev: {
|
||||
n8n-openai-adapter = self.packages.${final.stdenv.system}.default;
|
||||
};
|
||||
|
||||
# Proper NixOS module: consume with
|
||||
# imports = [ inputs.n8n-openai-adapter.nixosModules.default ];
|
||||
# services.n8n-openai-adapter = { enable = true; domain = "..."; port = 8134; };
|
||||
nixosModules.default = import ./nixos-module.nix;
|
||||
};
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
defexception EEx.SyntaxError, message: nil
|
||||
|
||||
defmodule EEx do
|
||||
@moduledoc ~S"""
|
||||
EEx stands for Embedded Elixir. It allows you to embed
|
||||
Elixir code inside a string in a robust way:
|
||||
|
||||
iex> EEx.eval_string "foo <%= bar %>", [bar: "baz"]
|
||||
"foo baz"
|
||||
|
||||
## API
|
||||
|
||||
This module provides 3 main APIs for you to use:
|
||||
|
||||
1) Evaluate a string (`eval_string`) or a file (`eval_file`)
|
||||
directly. This is the simplest API to use but also the
|
||||
slowest, since the code is evaluated and not compiled before;
|
||||
|
||||
2) Define a function from a string (`function_from_string`)
|
||||
or a file (`function_from_file`). This allows you to embed
|
||||
the template as a function inside a module which will then
|
||||
be compiled. This is the preferred API if you have access
|
||||
to the template at compilation time;
|
||||
|
||||
3) Compile a string (`compile_string`) or a file (`compile_file`)
|
||||
into Elixir syntax tree. This is the API used by both functions
|
||||
above and is available to you if you want to provide your own
|
||||
ways of handling the compiled template.
|
||||
|
||||
## Options
|
||||
|
||||
All functions in this module accepts EEx-related options.
|
||||
They are:
|
||||
|
||||
* `:line` - the line to be used as the template start.
|
||||
Defaults to 1;
|
||||
* `:file` - the file to be used in the template.
|
||||
Defaults to the given file the template is read from
|
||||
or to "nofile" when compiling from a string;
|
||||
* `:engine` - the EEx engine to be used for compilation.
|
||||
|
||||
## Engine
|
||||
|
||||
EEx has the concept of engines which allows you to modify or
|
||||
transform the code extracted from the given string or file.
|
||||
|
||||
By default, `EEx` uses the `EEx.SmartEngine` that provides some
|
||||
conveniences on top of the simple `EEx.Engine`.
|
||||
|
||||
### Tags
|
||||
|
||||
`EEx.SmartEngine` supports the following tags:
|
||||
|
||||
<% Elixir expression - inline with output %>
|
||||
<%= Elixir expression - replace with result %>
|
||||
<%% EEx quotation - returns the contents inside %>
|
||||
<%# Comments - they are discarded from source %>
|
||||
|
||||
All expressions that output something to the template
|
||||
**must** use the equals sign (`=`). Since everything in
|
||||
Elixir is a macro, there are no exceptions for this rule.
|
||||
For example, while some template languages would special-
|
||||
case `if` clauses, they are treated the same in EEx and
|
||||
also require `=` in order to have their result printed:
|
||||
|
||||
<%= if true do %>
|
||||
It is obviously true
|
||||
<% else %>
|
||||
This will never appear
|
||||
<% end %>
|
||||
|
||||
Notice that different engines may have different rules
|
||||
for each tag. Other tags may be added in future versions.
|
||||
|
||||
### Macros
|
||||
|
||||
`EEx.SmartEngine` also adds some macros to your template.
|
||||
An example is the `@` macro which allows easy data access
|
||||
in a template:
|
||||
|
||||
iex> EEx.eval_string "<%= @foo %>", assigns: [foo: 1]
|
||||
"1"
|
||||
|
||||
In other words, <%= @foo %> is simply translated to:
|
||||
|
||||
<%= Dict.get assigns, :foo %>
|
||||
|
||||
The assigns extension is useful when the number of variables
|
||||
required by the template is not specified at compilation time.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the string.
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> defmodule Sample do
|
||||
...> require EEx
|
||||
...> EEx.function_from_string :def, :sample, "<%= a + b %>", [:a, :b]
|
||||
...> end
|
||||
iex> Sample.sample(1, 2)
|
||||
"3"
|
||||
|
||||
"""
|
||||
defmacro function_from_string(kind, name, source, args \\ [], options \\ []) do
|
||||
quote bind_quoted: binding do
|
||||
info = Keyword.merge [file: __ENV__.file, line: __ENV__.line], options
|
||||
args = Enum.map args, fn arg -> {arg, [line: info[:line]], nil} end
|
||||
compiled = EEx.compile_string(source, info)
|
||||
|
||||
case kind do
|
||||
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
:defp -> defp(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the file contents.
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
This function is useful in case you have templates but
|
||||
you want to precompile inside a module for speed.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.eex
|
||||
<%= a + b %>
|
||||
|
||||
# sample.ex
|
||||
defmodule Sample do
|
||||
require EEx
|
||||
EEx.function_from_file :def, :sample, "sample.eex", [:a, :b]
|
||||
end
|
||||
|
||||
# iex
|
||||
Sample.sample(1, 2) #=> "3"
|
||||
|
||||
"""
|
||||
defmacro function_from_file(kind, name, file, args \\ [], options \\ []) do
|
||||
quote bind_quoted: binding do
|
||||
info = Keyword.merge options, [file: file, line: 1]
|
||||
args = Enum.map args, fn arg -> {arg, [line: 1], nil} end
|
||||
compiled = EEx.compile_file(file, info)
|
||||
|
||||
@file file
|
||||
case kind do
|
||||
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
:defp -> defp(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a string `source` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
"""
|
||||
def compile_string(source, options \\ []) do
|
||||
EEx.Compiler.compile(source, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a `filename` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
"""
|
||||
def compile_file(filename, options \\ []) do
|
||||
options = Keyword.merge options, [file: filename, line: 1]
|
||||
compile_string(File.read!(filename), options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a string `source` and evaluate the values using the `bindings`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> EEx.eval_string "foo <%= bar %>", [bar: "baz"]
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
def eval_string(source, bindings \\ [], options \\ []) do
|
||||
compiled = compile_string(source, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a `filename` and evaluate the values using the `bindings`.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.ex
|
||||
foo <%= bar %>
|
||||
|
||||
# iex
|
||||
EEx.eval_file "sample.ex", [bar: "baz"] #=> "foo baz"
|
||||
|
||||
"""
|
||||
def eval_file(filename, bindings \\ [], options \\ []) do
|
||||
options = Keyword.put options, :file, filename
|
||||
compiled = compile_file(filename, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
### Helpers
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
end
|
||||
@@ -1,116 +0,0 @@
|
||||
defmodule EEx.Compiler do
|
||||
@moduledoc false
|
||||
|
||||
# when changing this setting, don't forget to update the docs for EEx
|
||||
@default_engine EEx.SmartEngine
|
||||
|
||||
@doc """
|
||||
This is the compilation entry point. It glues the tokenizer
|
||||
and the engine together by handling the tokens and invoking
|
||||
the engine every time a full expression or text is received.
|
||||
"""
|
||||
def compile(source, opts) do
|
||||
file = opts[:file] || "nofile"
|
||||
line = opts[:line] || 1
|
||||
tokens = EEx.Tokenizer.tokenize(source, line)
|
||||
state = %{engine: opts[:engine] || @default_engine,
|
||||
file: file, line: line, quoted: [], start_line: nil}
|
||||
generate_buffer(tokens, "", [], state)
|
||||
end
|
||||
|
||||
# Generates the buffers by handling each expression from the tokenizer
|
||||
|
||||
defp generate_buffer([{:text, chars}|t], buffer, scope, state) do
|
||||
buffer = state.engine.handle_text(buffer, String.from_char_data!(chars))
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:expr, line, mark, chars}|t], buffer, scope, state) do
|
||||
expr = Code.string_to_quoted!(chars, [line: line, file: state.file])
|
||||
buffer = state.engine.handle_expr(buffer, mark, expr)
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:start_expr, start_line, mark, chars}|t], buffer, scope, state) do
|
||||
{contents, line, t} = look_ahead_text(t, start_line, chars)
|
||||
{contents, t} = generate_buffer(t, "", [contents|scope],
|
||||
%{state | quoted: [], line: line, start_line: start_line})
|
||||
buffer = state.engine.handle_expr(buffer, mark, contents)
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:middle_expr, line, _, chars}|t], buffer, [current|scope], state) do
|
||||
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
|
||||
generate_buffer(t, "", [wrapped|scope], %{state | line: line})
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, _, chars}|t], buffer, [current|_], state) do
|
||||
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
|
||||
tuples = Code.string_to_quoted!(wrapped, [line: state.start_line, file: state.file])
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, t}
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, _, chars}|_], _buffer, [], _state) do
|
||||
raise EEx.SyntaxError, message: "unexpected token: #{inspect chars} at line #{inspect line}"
|
||||
end
|
||||
|
||||
defp generate_buffer([], buffer, [], state) do
|
||||
state.engine.handle_body(buffer)
|
||||
end
|
||||
|
||||
defp generate_buffer([], _buffer, _scope, _state) do
|
||||
raise EEx.SyntaxError, message: "unexpected end of string. expecting a closing <% end %>."
|
||||
end
|
||||
|
||||
# Creates a placeholder and wrap it inside the expression block
|
||||
|
||||
defp wrap_expr(current, line, buffer, chars, state) do
|
||||
new_lines = List.duplicate(?\n, line - state.line)
|
||||
key = length(state.quoted)
|
||||
placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');'
|
||||
{current ++ placeholder ++ new_lines ++ chars,
|
||||
%{state | quoted: [{key, buffer}|state.quoted]}}
|
||||
end
|
||||
|
||||
# Look text ahead on expressions
|
||||
|
||||
defp look_ahead_text([{:text, text}, {:middle_expr, line, _, chars}|t]=list, start, contents) do
|
||||
if only_spaces?(text) do
|
||||
{contents ++ text ++ chars, line, t}
|
||||
else
|
||||
{contents, start, list}
|
||||
end
|
||||
end
|
||||
|
||||
defp look_ahead_text(t, start, contents) do
|
||||
{contents, start, t}
|
||||
end
|
||||
|
||||
defp only_spaces?(chars) do
|
||||
Enum.all?(chars, &(&1 in [?\s, ?\t, ?\r, ?\n]))
|
||||
end
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
{^key, value} = List.keyfind quoted, key, 0
|
||||
value
|
||||
end
|
||||
|
||||
defp insert_quoted({left, line, right}, quoted) do
|
||||
{insert_quoted(left, quoted), line, insert_quoted(right, quoted)}
|
||||
end
|
||||
|
||||
defp insert_quoted({left, right}, quoted) do
|
||||
{insert_quoted(left, quoted), insert_quoted(right, quoted)}
|
||||
end
|
||||
|
||||
defp insert_quoted(list, quoted) when is_list(list) do
|
||||
Enum.map list, &insert_quoted(&1, quoted)
|
||||
end
|
||||
|
||||
defp insert_quoted(other, _quoted) do
|
||||
other
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
defmodule EEx.Engine do
|
||||
@moduledoc ~S"""
|
||||
This is the basic EEx engine that ships with Elixir.
|
||||
An engine needs to implement three functions:
|
||||
|
||||
* `handle_body(quoted)` - receives the final built quoted
|
||||
expression, should do final post-processing and return a
|
||||
quoted expression;
|
||||
|
||||
* `handle_text(buffer, text)` - it receives the buffer,
|
||||
the text and must return a new quoted expression;
|
||||
|
||||
* `handle_expr(buffer, marker, expr)` - it receives the buffer,
|
||||
the marker, the expr and must return a new quoted expression;
|
||||
|
||||
The marker is what follows exactly after `<%`. For example,
|
||||
`<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
|
||||
as marker. The allowed markers so far are:
|
||||
|
||||
* `""`
|
||||
* `"="`
|
||||
|
||||
Read `handle_expr/3` below for more information about the markers
|
||||
implemented by default by this engine.
|
||||
"""
|
||||
|
||||
use Behaviour
|
||||
|
||||
defcallback handle_body(Macro.t) :: Macro.t
|
||||
defcallback handle_text(Macro.t, binary) :: Macro.t
|
||||
defcallback handle_expr(Macro.t, binary, Macro.t) :: Macro.t
|
||||
|
||||
@doc """
|
||||
The default implementation implementation simply returns the
|
||||
given expression.
|
||||
"""
|
||||
def handle_body(quoted) do
|
||||
quoted
|
||||
end
|
||||
|
||||
@doc """
|
||||
The default implementation simply concatenates text to the buffer.
|
||||
"""
|
||||
def handle_text(buffer, text) do
|
||||
quote do: unquote(buffer) <> unquote(text)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implements expressions according to the markers.
|
||||
|
||||
<% Elixir expression - inline with output %>
|
||||
<%= Elixir expression - replace with result %>
|
||||
|
||||
All other markers are not implemented by this engine.
|
||||
"""
|
||||
def handle_expr(buffer, "=", expr) do
|
||||
quote do
|
||||
tmp = unquote(buffer)
|
||||
tmp <> to_string(unquote(expr))
|
||||
end
|
||||
end
|
||||
|
||||
def handle_expr(buffer, "", expr) do
|
||||
quote do
|
||||
tmp = unquote(buffer)
|
||||
unquote(expr)
|
||||
tmp
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,116 +0,0 @@
|
||||
defmodule EEx.TransformerEngine do
|
||||
@moduledoc """
|
||||
An abstract engine that is meant to be used and
|
||||
built upon in other modules. This engine implements
|
||||
the `EEx.Engine` behaviour and provides a `transform`
|
||||
overridable directive that allows a developer to
|
||||
customize the expression returned by the engine.
|
||||
|
||||
Check `EEx.AssignsEngine` and `EEx.SmartEngine` for
|
||||
examples of using this module.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@behaviour EEx.Engine
|
||||
|
||||
def handle_body(body) do
|
||||
EEx.Engine.handle_body(body)
|
||||
end
|
||||
|
||||
def handle_text(buffer, text) do
|
||||
EEx.Engine.handle_text(buffer, text)
|
||||
end
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
EEx.Engine.handle_expr(buffer, mark, transform(expr))
|
||||
end
|
||||
|
||||
defp transform({a, b, c}) do
|
||||
{transform(a), b, transform(c)}
|
||||
end
|
||||
|
||||
defp transform({a, b}) do
|
||||
{transform(a), transform(b)}
|
||||
end
|
||||
|
||||
defp transform(list) when is_list(list) do
|
||||
for i <- list, do: transform(i)
|
||||
end
|
||||
|
||||
defp transform(other) do
|
||||
other
|
||||
end
|
||||
|
||||
defoverridable [transform: 1, handle_body: 1, handle_expr: 3, handle_text: 2]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx.AssignsEngine do
|
||||
@moduledoc """
|
||||
An abstract engine that, when used with the
|
||||
`TransformerEngine`, allows a developer to access
|
||||
assigns using `@` as syntax.
|
||||
|
||||
This engine is included by default on the SmartEngine.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyEngine do
|
||||
use EEx.TransformerEngine
|
||||
use EEx.AssignsEngine
|
||||
end
|
||||
|
||||
iex> EEx.eval_string("<%= @foo %>", assigns: [foo: 1])
|
||||
"1"
|
||||
|
||||
In the example above, we can access the value `foo` under
|
||||
the binding `assigns` using `@foo`. This is useful when
|
||||
a template, after compiled, may receive different assigns
|
||||
and the developer don't want to recompile it for each
|
||||
variable set.
|
||||
|
||||
Assigns can also be used when compiled to a function:
|
||||
|
||||
# sample.eex
|
||||
<%= @a + @b %>
|
||||
|
||||
# sample.ex
|
||||
defmodule Sample do
|
||||
require EEx
|
||||
EEx.function_from_file :def, :sample, "sample.eex", [:assigns]
|
||||
end
|
||||
|
||||
# iex
|
||||
Sample.sample(a: 1, b: 2) #=> "3"
|
||||
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote unquote: false do
|
||||
defp transform({:@, line, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
|
||||
quote do: Dict.get(var!(assigns), unquote(name))
|
||||
end
|
||||
|
||||
defp transform(arg) do
|
||||
super(arg)
|
||||
end
|
||||
|
||||
defoverridable [transform: 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx.SmartEngine do
|
||||
use EEx.TransformerEngine
|
||||
use EEx.AssignsEngine
|
||||
|
||||
@moduledoc """
|
||||
An engine meant for end-user usage that includes
|
||||
`EEx.AssignsEngine` and other conveniences. Read
|
||||
`EEx.AssignsEngine` for examples.
|
||||
"""
|
||||
end
|
||||
@@ -1,161 +0,0 @@
|
||||
defmodule EEx.Tokenizer do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Tokenizes the given char list or binary.
|
||||
It returns 4 different types of tokens as result:
|
||||
|
||||
* {:text, contents}
|
||||
* {:expr, line, marker, contents}
|
||||
* {:start_expr, line, marker, contents}
|
||||
* {:middle_expr, line, marker, contents}
|
||||
* {:end_expr, line, marker, contents}
|
||||
|
||||
"""
|
||||
def tokenize(bin, line) when is_binary(bin) do
|
||||
tokenize(List.from_char_data!(bin), line)
|
||||
end
|
||||
|
||||
def tokenize(list, line) do
|
||||
Enum.reverse(tokenize(list, line, [], []))
|
||||
end
|
||||
|
||||
defp tokenize('<%%' ++ t, line, buffer, acc) do
|
||||
{buffer, new_line, rest} = tokenize_expr t, line, [?%, ?<|buffer]
|
||||
tokenize rest, new_line, [?>, ?%|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize('<%#' ++ t, line, buffer, acc) do
|
||||
{_, new_line, rest} = tokenize_expr t, line, []
|
||||
tokenize rest, new_line, buffer, acc
|
||||
end
|
||||
|
||||
defp tokenize('<%' ++ t, line, buffer, acc) do
|
||||
{marker, t} = retrieve_marker(t)
|
||||
{expr, new_line, rest} = tokenize_expr t, line, []
|
||||
|
||||
token = token_name(expr)
|
||||
acc = tokenize_text(buffer, acc)
|
||||
final = {token, line, marker, Enum.reverse(expr)}
|
||||
tokenize rest, new_line, [], [final | acc]
|
||||
end
|
||||
|
||||
defp tokenize('\n' ++ t, line, buffer, acc) do
|
||||
tokenize t, line + 1, [?\n|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize([h|t], line, buffer, acc) do
|
||||
tokenize t, line, [h|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize([], _line, buffer, acc) do
|
||||
tokenize_text(buffer, acc)
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker('=' ++ t) do
|
||||
{"=", t}
|
||||
end
|
||||
|
||||
defp retrieve_marker(t) do
|
||||
{"", t}
|
||||
end
|
||||
|
||||
# Tokenize an expression until we find %>
|
||||
|
||||
defp tokenize_expr([?%, ?>|t], line, buffer) do
|
||||
{buffer, line, t}
|
||||
end
|
||||
|
||||
defp tokenize_expr('\n' ++ t, line, buffer) do
|
||||
tokenize_expr t, line + 1, [?\n|buffer]
|
||||
end
|
||||
|
||||
defp tokenize_expr([h|t], line, buffer) do
|
||||
tokenize_expr t, line, [h|buffer]
|
||||
end
|
||||
|
||||
defp tokenize_expr([], _line, _buffer) do
|
||||
raise EEx.SyntaxError, message: "missing token: %>"
|
||||
end
|
||||
|
||||
# Receive an expression content and check
|
||||
# if it is a start, middle or an end token.
|
||||
#
|
||||
# Start tokens finish with `do` and `fn ->`
|
||||
# Middle tokens are marked with `->` or keywords
|
||||
# End tokens contain only the end word
|
||||
|
||||
defp token_name([h|t]) when h in [?\s, ?\t] do
|
||||
token_name(t)
|
||||
end
|
||||
|
||||
defp token_name('od' ++ [h|_]) when h in [?\s, ?\t, ?)] do
|
||||
:start_expr
|
||||
end
|
||||
|
||||
defp token_name('>-' ++ rest) do
|
||||
rest = Enum.reverse(rest)
|
||||
|
||||
# Tokenize the remaining passing check_terminators as
|
||||
# false, which relax the tokenizer to not error on
|
||||
# unmatched pairs. Then, we check if there is a "fn"
|
||||
# token and, if so, it is not followed by an "end"
|
||||
# token. If this is the case, we are on a start expr.
|
||||
case :elixir_tokenizer.tokenize(rest, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, tokens} ->
|
||||
tokens = Enum.reverse(tokens)
|
||||
fn_index = fn_index(tokens)
|
||||
|
||||
if fn_index && end_index(tokens) > fn_index do
|
||||
:start_expr
|
||||
else
|
||||
:middle_expr
|
||||
end
|
||||
_error ->
|
||||
:middle_expr
|
||||
end
|
||||
end
|
||||
|
||||
defp token_name('esle' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('retfa' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('hctac' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('eucser' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('dne' ++ t), do: check_spaces(t, :end_expr)
|
||||
|
||||
defp token_name(_) do
|
||||
:expr
|
||||
end
|
||||
|
||||
defp fn_index(tokens) do
|
||||
Enum.find_index tokens, fn
|
||||
{:fn_paren, _} -> true
|
||||
{:fn, _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp end_index(tokens) do
|
||||
Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
|
||||
end
|
||||
|
||||
defp check_spaces(string, token) do
|
||||
if Enum.all?(string, &(&1 in [?\s, ?\t])) do
|
||||
token
|
||||
else
|
||||
:expr
|
||||
end
|
||||
end
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
|
||||
defp tokenize_text([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp tokenize_text(buffer, acc) do
|
||||
[{:text, Enum.reverse(buffer)} | acc]
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
defmodule EEx.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :eex,
|
||||
version: System.version,
|
||||
build_per_environment: false]
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
Code.require_file "../test_helper.exs", __DIR__
|
||||
|
||||
defmodule EEx.SmartEngineTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "evaluates simple string" do
|
||||
assert_eval "foo bar", "foo bar"
|
||||
end
|
||||
|
||||
test "evaluates with assigns as keywords" do
|
||||
assert_eval "1", "<%= @foo %>", assigns: [foo: 1]
|
||||
end
|
||||
|
||||
test "evaluates with assigns as a map" do
|
||||
assert_eval "1", "<%= @foo %>", assigns: %{foo: 1}
|
||||
end
|
||||
|
||||
test "evaluates with loops" do
|
||||
assert_eval "1\n2\n3\n", "<%= for x <- [1, 2, 3] do %><%= x %>\n<% end %>"
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file)
|
||||
assert result == expected
|
||||
end
|
||||
end
|
||||
@@ -1,105 +0,0 @@
|
||||
Code.require_file "../test_helper.exs", __DIR__
|
||||
|
||||
defmodule EEx.TokenizerTest do
|
||||
use ExUnit.Case, async: true
|
||||
require EEx.Tokenizer, as: T
|
||||
|
||||
test "simple chars lists" do
|
||||
assert T.tokenize('foo', 1) == [ {:text, 'foo'} ]
|
||||
end
|
||||
|
||||
test "simple strings" do
|
||||
assert T.tokenize("foo", 1) == [ {:text, 'foo'} ]
|
||||
end
|
||||
|
||||
test "strings with embedded code" do
|
||||
assert T.tokenize('foo <% bar %>', 1) == [ {:text, 'foo '}, {:expr, 1, "", ' bar '} ]
|
||||
end
|
||||
|
||||
test "strings with embedded equals code" do
|
||||
assert T.tokenize('foo <%= bar %>', 1) == [ {:text, 'foo '}, {:expr, 1, "=", ' bar '} ]
|
||||
end
|
||||
|
||||
test "strings with more than one line" do
|
||||
assert T.tokenize('foo\n<%= bar %>', 1) == [ {:text, 'foo\n'}, {:expr, 2, "=", ' bar '} ]
|
||||
end
|
||||
|
||||
test "strings with more than one line and expression with more than one line" do
|
||||
string = '''
|
||||
foo <%= bar
|
||||
|
||||
baz %>
|
||||
<% foo %>
|
||||
'''
|
||||
|
||||
assert T.tokenize(string, 1) == [
|
||||
{:text, 'foo '},
|
||||
{:expr, 1, "=", ' bar\n\nbaz '},
|
||||
{:text, '\n'},
|
||||
{:expr, 4, "", ' foo '},
|
||||
{:text, '\n'}
|
||||
]
|
||||
end
|
||||
|
||||
test "quotation" do
|
||||
assert T.tokenize('foo <%% true %>', 1) == [
|
||||
{:text, 'foo <% true %>'}
|
||||
]
|
||||
end
|
||||
|
||||
test "quotation with do/end" do
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1) == [
|
||||
{:text, 'foo <% true do %>bar<% end %>'}
|
||||
]
|
||||
end
|
||||
|
||||
test "comments" do
|
||||
assert T.tokenize('foo <%# true %>', 1) == [
|
||||
{:text, 'foo '}
|
||||
]
|
||||
end
|
||||
|
||||
test "comments with do/end" do
|
||||
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == [
|
||||
{:text, 'foo bar'}
|
||||
]
|
||||
end
|
||||
|
||||
test "strings with embedded do end" do
|
||||
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, "", ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, "", ' end '}
|
||||
]
|
||||
end
|
||||
|
||||
test "strings with embedded -> end" do
|
||||
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) == [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, "", ' cond do '},
|
||||
{:middle_expr, 1, "", ' false -> '},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, "", ' true -> '},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, "", ' end '}
|
||||
]
|
||||
end
|
||||
|
||||
test "strings with embedded keywords blocks" do
|
||||
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, "", ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, "", ' else '},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, "", ' end '}
|
||||
]
|
||||
end
|
||||
|
||||
test "raise syntax error when there is start mark and no end mark" do
|
||||
assert_raise EEx.SyntaxError, "missing token: %>", fn ->
|
||||
T.tokenize('foo <% :bar', 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,383 +0,0 @@
|
||||
Code.require_file "test_helper.exs", __DIR__
|
||||
|
||||
require EEx
|
||||
|
||||
defmodule EExText.Compiled do
|
||||
def before_compile do
|
||||
fill_in_stacktrace
|
||||
{__ENV__.line, hd(tl(System.stacktrace))}
|
||||
end
|
||||
{:erlang, 1, 2}.tuple_to_list
|
||||
EEx.function_from_string :def, :string_sample, "<%= a + b %>", [:a, :b]
|
||||
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
||||
EEx.function_from_file :defp, :private_file_sample, filename, [:bar]
|
||||
def file_sample(arg), do: private_file_sample(arg)
|
||||
|
||||
def after_compile do
|
||||
fill_in_stacktrace
|
||||
{__ENV__.line, hd(tl(System.stacktrace))}
|
||||
end
|
||||
|
||||
@file "unknown"
|
||||
def unknown do
|
||||
fill_in_stacktrace
|
||||
{__ENV__.line, hd(tl(System.stacktrace))}
|
||||
end
|
||||
|
||||
defp fill_in_stacktrace do
|
||||
try do
|
||||
:erlang.error "failed"
|
||||
catch
|
||||
:error, _ -> System.stacktrace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Clause do
|
||||
defmacro defclause(expr, block) do
|
||||
quote do
|
||||
def unquote(expr), unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EExTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
doctest EEx
|
||||
doctest EEx.AssignsEngine
|
||||
|
||||
test "evaluates simple string" do
|
||||
assert_eval "foo bar", "foo bar"
|
||||
end
|
||||
|
||||
test "evaluates with embedded" do
|
||||
assert_eval "foo bar", "foo <%= :bar %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded and the binding" do
|
||||
assert EEx.eval_string("foo <%= bar %>", [bar: 1]) == "foo 1"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end" do
|
||||
assert_eval "foo bar", "foo <%= if true do %>bar<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end and eval the expression" do
|
||||
assert_eval "foo ", "foo <%= if false do %>bar<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end and nested print expression" do
|
||||
assert_eval "foo bar", "foo <%= if true do %><%= :bar %><% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end and nested expressions" do
|
||||
assert_eval "foo bar baz", "foo <%= if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>"
|
||||
assert Process.get(:eex_text) == 1
|
||||
end
|
||||
|
||||
test "evaluates with embedded middle expression" do
|
||||
assert_eval "foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded middle expression and eval the expression" do
|
||||
assert_eval "foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with nested start expression" do
|
||||
assert_eval "foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with nested middle expression" do
|
||||
assert_eval "foo baz", "foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with defined variable" do
|
||||
assert_eval "foo 1", "foo <% bar = 1 %><%= bar %>"
|
||||
end
|
||||
|
||||
test "evaluates with require code" do
|
||||
assert_eval "foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>"
|
||||
end
|
||||
|
||||
test "evaluates with end of token" do
|
||||
assert_eval "foo bar %>", "foo bar %>"
|
||||
end
|
||||
|
||||
test "raises a syntax error when the token is invalid" do
|
||||
assert_raise EEx.SyntaxError, "missing token: %>", fn ->
|
||||
EEx.compile_string "foo <%= bar"
|
||||
end
|
||||
end
|
||||
|
||||
test "raises a syntax error when end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "unexpected token: ' end ' at line 1", fn ->
|
||||
EEx.compile_string "foo <% end %>"
|
||||
end
|
||||
end
|
||||
|
||||
test "raises a syntax error when start expression is found without an end expression" do
|
||||
assert_raise EEx.SyntaxError, "unexpected end of string. expecting a closing <% end %>.", fn ->
|
||||
EEx.compile_string "foo <% if true do %>"
|
||||
end
|
||||
end
|
||||
|
||||
test "raises a syntax error when nested end expression is found without an start expression" do
|
||||
assert_raise EEx.SyntaxError, "unexpected token: ' end ' at line 1", fn ->
|
||||
EEx.compile_string "foo <% if true do %><% end %><% end %>"
|
||||
end
|
||||
end
|
||||
|
||||
test "respects line numbers" do
|
||||
expected = """
|
||||
foo
|
||||
2
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line numbers inside nested expressions" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
3
|
||||
|
||||
5
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= if true do %>
|
||||
<%= __ENV__.line %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line numbers inside start expression" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
5
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= if __ENV__.line == 2 do %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line numbers inside middle expression with ->" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
7
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= cond do %>
|
||||
<% false -> %> false
|
||||
<% __ENV__.line == 4 -> %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line number inside middle expressions with keywords" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
5
|
||||
|
||||
7
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= if false do %>
|
||||
<%= __ENV__.line %>
|
||||
<% else %>
|
||||
<%= __ENV__.line %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "properly handle functions" do
|
||||
expected = """
|
||||
|
||||
Number 1
|
||||
|
||||
Number 2
|
||||
|
||||
Number 3
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
<%= Enum.map [1, 2, 3], fn x -> %>
|
||||
Number <%= x %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "do not consider already finished functions" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= cond do %>
|
||||
<% false -> %> false
|
||||
<% fn -> 1 end -> %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "evaluates nested do expressions" do
|
||||
string = """
|
||||
<% y = ["a", "b", "c"] %>
|
||||
<%= cond do %>
|
||||
<% "a" in y -> %>
|
||||
Good
|
||||
<% true -> %>
|
||||
<% if true do %>true<% else %>false<% end %>
|
||||
Bad
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval "\n\n Good\n \n", string
|
||||
end
|
||||
|
||||
test "for comprehensions" do
|
||||
string = """
|
||||
<%= for _name <- packages || [] do %>
|
||||
<% end %>
|
||||
<%= all || :done %>
|
||||
"""
|
||||
assert_eval "\ndone\n", string, packages: nil, all: nil
|
||||
end
|
||||
|
||||
test "unicode" do
|
||||
template = """
|
||||
• <%= "•" %> •
|
||||
<%= "Jößé Vâlìm" %> Jößé Vâlìm
|
||||
"""
|
||||
result = EEx.eval_string(template)
|
||||
assert result == " • • •\n Jößé Vâlìm Jößé Vâlìm\n"
|
||||
end
|
||||
|
||||
test "evaluates the source from a given file" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template.eex")
|
||||
result = EEx.eval_file(filename)
|
||||
assert result == "foo bar.\n"
|
||||
end
|
||||
|
||||
test "evaluates the source from a given file with bindings" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
||||
result = EEx.eval_file(filename, [bar: 1])
|
||||
assert result == "foo 1\n"
|
||||
end
|
||||
|
||||
test "raises an Exception when there's an error with the given file" do
|
||||
assert_raise File.Error, "could not read file non-existent.eex: no such file or directory", fn ->
|
||||
filename = "non-existent.eex"
|
||||
EEx.compile_file(filename)
|
||||
end
|
||||
end
|
||||
|
||||
test "defined from string" do
|
||||
assert EExText.Compiled.string_sample(1, 2) == "3"
|
||||
end
|
||||
|
||||
test "defined from file" do
|
||||
assert EExText.Compiled.file_sample(1) == "foo 1\n"
|
||||
end
|
||||
|
||||
test "defined from file do not affect backtrace" do
|
||||
assert EExText.Compiled.before_compile ==
|
||||
{8,
|
||||
{EExText.Compiled,
|
||||
:before_compile,
|
||||
0,
|
||||
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 7]
|
||||
}
|
||||
}
|
||||
|
||||
assert EExText.Compiled.after_compile ==
|
||||
{19,
|
||||
{EExText.Compiled,
|
||||
:after_compile,
|
||||
0,
|
||||
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 18]
|
||||
}
|
||||
}
|
||||
|
||||
assert EExText.Compiled.unknown ==
|
||||
{25,
|
||||
{EExText.Compiled,
|
||||
:unknown,
|
||||
0,
|
||||
[file: 'unknown', line: 24]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defmodule TestEngine do
|
||||
@behaviour EEx.Engine
|
||||
|
||||
def handle_body(body) do
|
||||
{:wrapped, body}
|
||||
end
|
||||
|
||||
def handle_text(buffer, text) do
|
||||
EEx.Engine.handle_text(buffer, text)
|
||||
end
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
EEx.Engine.handle_expr(buffer, mark, expr)
|
||||
end
|
||||
end
|
||||
|
||||
test "calls handle_body" do
|
||||
assert {:wrapped, "foo"} = EEx.eval_string("foo", [], engine: TestEngine)
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.Engine)
|
||||
assert result == expected
|
||||
end
|
||||
end
|
||||
-1
@@ -1 +0,0 @@
|
||||
foo <%= if true do %>bar.<% end %>
|
||||
@@ -1 +0,0 @@
|
||||
foo <%= bar %>
|
||||
@@ -1 +0,0 @@
|
||||
ExUnit.start [trace: "--trace" in System.argv]
|
||||
@@ -1,68 +0,0 @@
|
||||
-define(m(M, K), maps:get(K, M)).
|
||||
-define(line(Opts), elixir_utils:get_line(Opts)).
|
||||
|
||||
-record(elixir_scope, {
|
||||
context=nil, %% can be match, guards or nil
|
||||
extra=nil, %% extra information about the context, like fn_match and map_key
|
||||
noname=false, %% when true, don't add new names (used by try)
|
||||
super=false, %% when true, it means super was invoked
|
||||
caller=false, %% when true, it means caller was invoked
|
||||
return=true, %% when true, the return value is used
|
||||
module=nil, %% the current module
|
||||
function=nil, %% the current function
|
||||
vars=[], %% a dict 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
|
||||
counter=[], %% a dict counting the variables defined
|
||||
file=(<<"nofile">>) %% the current scope filename
|
||||
}).
|
||||
|
||||
-record(elixir_quote, {
|
||||
line=false,
|
||||
keep=false,
|
||||
context=nil,
|
||||
vars_hygiene=true,
|
||||
aliases_hygiene=true,
|
||||
imports_hygiene=true,
|
||||
unquote=true,
|
||||
unquoted=false,
|
||||
escape=false
|
||||
}).
|
||||
|
||||
-record(elixir_tokenizer, {
|
||||
file,
|
||||
terminators=[],
|
||||
check_terminators=true,
|
||||
existing_atoms_only=false
|
||||
}).
|
||||
|
||||
%% Used in tokenization and interpolation
|
||||
|
||||
%% Numbers
|
||||
-define(is_hex(S), ?is_digit(S) orelse (S >= $A andalso S =< $F) orelse (S >= $a andalso S =< $f)).
|
||||
-define(is_bin(S), S >= $0 andalso S =< $1).
|
||||
-define(is_octal(S), S >= $0 andalso S =< $7).
|
||||
-define(is_leading_octal(S), S >= $0 andalso S =< $3).
|
||||
|
||||
%% Digits and letters
|
||||
-define(is_digit(S), S >= $0 andalso S =< $9).
|
||||
-define(is_upcase(S), S >= $A andalso S =< $Z).
|
||||
-define(is_downcase(S), S >= $a andalso S =< $z).
|
||||
|
||||
%% Atoms
|
||||
-define(is_atom_start(S), ?is_quote(S) orelse ?is_upcase(S) orelse ?is_downcase(S) orelse (S == $_)).
|
||||
-define(is_atom(S), ?is_identifier(S) orelse (S == $@)).
|
||||
|
||||
-define(is_identifier(S), ?is_digit(S) orelse ?is_upcase(S) orelse ?is_downcase(S) orelse (S == $_)).
|
||||
-define(is_sigil(S), (S == $/) orelse (S == $<) orelse (S == $") orelse (S == $') orelse
|
||||
(S == $[) orelse (S == $() orelse (S == ${) orelse (S == $|)).
|
||||
|
||||
%% Quotes
|
||||
-define(is_quote(S), S == $" orelse S == $').
|
||||
|
||||
%% Spaces
|
||||
-define(is_horizontal_space(S), (S == $\s) orelse (S == $\t)).
|
||||
-define(is_vertical_space(S), (S == $\r) orelse (S == $\n)).
|
||||
-define(is_space(S), ?is_horizontal_space(S) orelse ?is_vertical_space(S)).
|
||||
@@ -1,67 +0,0 @@
|
||||
import Kernel, except: [access: 2]
|
||||
|
||||
defprotocol Access do
|
||||
@moduledoc """
|
||||
The Access protocol is the underlying protocol invoked
|
||||
when the brackets syntax is used. For instance, `foo[bar]`
|
||||
is translated to `access foo, bar` which, by default,
|
||||
invokes the `Access.access` protocol.
|
||||
|
||||
This protocol is implemented by default for Lists, Maps
|
||||
and dictionary like types:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
1
|
||||
|
||||
iex> map = %{a: 1, b: 2}
|
||||
iex> map[:a]
|
||||
1
|
||||
|
||||
iex> star_ratings = %{1.0 => "★", 1.5 => "★☆", 2.0 => "★★"}
|
||||
iex> star_ratings[1.5]
|
||||
"★☆"
|
||||
|
||||
The key access must be implemented using the `===` operator.
|
||||
This protocol is limited and is implemented only for the
|
||||
following built-in types: keywords, records and functions.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Receives the element being accessed and the access item.
|
||||
"""
|
||||
def access(container, key)
|
||||
end
|
||||
|
||||
defimpl Access, for: List do
|
||||
def access(dict, key) when is_atom(key) do
|
||||
case :lists.keyfind(key, 1, dict) do
|
||||
{^key, value} -> value
|
||||
false -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def access(_dict, key) do
|
||||
raise ArgumentError, message: "the access protocol for lists expect the key to be an atom, got: #{inspect key}"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: Map do
|
||||
def access(map, key) do
|
||||
case :maps.find(key, map) do
|
||||
{:ok, value} -> value
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: Atom do
|
||||
def access(nil, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
def access(atom, _) do
|
||||
raise "The access protocol can only be invoked for atoms at " <>
|
||||
"compilation time, tried to invoke it for #{inspect atom}"
|
||||
end
|
||||
end
|
||||
@@ -1,403 +0,0 @@
|
||||
defmodule Application do
|
||||
@moduledoc """
|
||||
A module for working with applications and defining application callbacks.
|
||||
|
||||
In Elixir (actually, in Erlang/OTP), an application is a component
|
||||
implementing some specific functionality, that can be started and stopped
|
||||
as a unit, and which can be re-used in other systems as well.
|
||||
|
||||
Applications are defined with an application file named `APP.app` where
|
||||
`APP` is the APP name, usually in `underscore_case` convention. The
|
||||
application file must reside in the same `ebin` directory as the
|
||||
application's modules bytecode.
|
||||
|
||||
In Elixir, Mix is responsible for compiling your source code and
|
||||
generating your application `.app` file. Furthermore, Mix is also
|
||||
responsible for configuring, starting and stoping your application
|
||||
and its dependencies. For this reason, this documentation will focus
|
||||
on the remaining aspects of your application: the application environment,
|
||||
and the application callback module.
|
||||
|
||||
You can learn more about Mix compilation of `.app` files by typing
|
||||
`mix help compile.app`.
|
||||
|
||||
## Application environment
|
||||
|
||||
Once an application is started, OTP provides an application environment
|
||||
that can be used to configure applications.
|
||||
|
||||
Assuming you are inside a Mix project, you can edit your application
|
||||
function in the `mix.exs` file to the following:
|
||||
|
||||
def application do
|
||||
[env: [hello: :world]]
|
||||
end
|
||||
|
||||
In the application function, we can define the default environment values
|
||||
for our application. By starting your application with `iex -S mix`, you
|
||||
can access the default value:
|
||||
|
||||
Application.get_env(:APP_NAME, :hello)
|
||||
#=> {:ok, :hello}
|
||||
|
||||
It is also possible to put and delete values from the application value,
|
||||
including new values that are not defined in the environment file (although
|
||||
those should be avoided).
|
||||
|
||||
In the future, we plan to support configuration files which allows
|
||||
developers to configure the environment of their dependencies.
|
||||
|
||||
Keep in mind that each application is responsible for its environment.
|
||||
Do not use the fucntions in this module for directly access or modify
|
||||
the environment of other application (as it may lead to inconsistent
|
||||
data in the application environment).
|
||||
|
||||
## Application module callback
|
||||
|
||||
Often times, an application defines a supervision tree that must be started
|
||||
and stopped when the application starts and stops. For such, we need to
|
||||
define an application module callback. The first step is to define the
|
||||
module callback in the application definition in the `mix.exs` file:
|
||||
|
||||
def application do
|
||||
[mod: {MyApp, []}]
|
||||
end
|
||||
|
||||
Our application now requires the `MyApp` module to provide an application
|
||||
callback. This can be done by invoking `use Application` in that module
|
||||
and defining a `start/2` callback, for example:
|
||||
|
||||
defmodule MyApp do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
MyApp.Supervisor.start_link()
|
||||
end
|
||||
end
|
||||
|
||||
`start/2` most commonly returns `{:ok, pid}` or `{:ok, pid, state}` where
|
||||
`pid` identifies the supervision tree and the state is the application state.
|
||||
`args` is second element of the tuple given to the `:mod` option.
|
||||
|
||||
The `type` passed into `start/2` is usually `:normal` unless in a distributed
|
||||
setup where applications takeover and failovers are configured. This particular
|
||||
aspect of applications can be read with more detail in the OTP documentation:
|
||||
|
||||
* http://www.erlang.org/doc/man/application.html
|
||||
* http://www.erlang.org/doc/design_principles/applications.html
|
||||
|
||||
A developer may also implement the `stop/1` callback (automatically defined
|
||||
by `use Application`) which does any application cleanup. It receives the
|
||||
application state and can return any value. Notice that shutting down the
|
||||
supervisor is automatically handled by the VM;
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :application
|
||||
|
||||
@doc false
|
||||
def stop(_state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defoverridable [stop: 1]
|
||||
end
|
||||
end
|
||||
|
||||
@type app :: atom
|
||||
@type key :: atom
|
||||
@type value :: term
|
||||
@type start_type :: :permanent | :transient | :temporary
|
||||
|
||||
@doc """
|
||||
Returns all key-value pairs for `app`.
|
||||
"""
|
||||
@spec get_all_env(app) :: [{key,value}]
|
||||
def get_all_env(app) do
|
||||
:application.get_all_env(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for `key` in `app`'s environment.
|
||||
|
||||
If the specified application is not loaded, or the configuration parameter
|
||||
does not exist, the function returns the `default` value.
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) do
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, value} -> value
|
||||
:undefined -> default
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for `key` in `app`'s environment in a tuple.
|
||||
|
||||
If the specified application is not loaded, or the configuration parameter
|
||||
does not exist, the function returns `:error`.
|
||||
"""
|
||||
@spec fetch_env(app, key) :: {:ok, value} | :error
|
||||
def fetch_env(app, key) do
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, value} -> {:ok, value}
|
||||
:undefined -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the `value` in `key` for the given `app`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:timeout` - the timeout for the change (defaults to 5000ms);
|
||||
|
||||
* `:persistent` - persists the given value on application load and reloads;
|
||||
|
||||
If `put_env/4` is called before the application is loaded, the application
|
||||
environment values specified in the `.app` file will override the ones
|
||||
previously set.
|
||||
|
||||
The persistent option can be set to true when there is a need to guarantee
|
||||
parameters set with this function will not be overridden by the ones defined
|
||||
in the application resource file on load. This means persistent values will
|
||||
stick after the application is loaded and also on application reload.
|
||||
"""
|
||||
@spec put_env(app, key, value, [timeout: timeout, persistent: boolean]) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) do
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the `key` from the given `app` environment.
|
||||
|
||||
See `put_env/4` for a description of the options.
|
||||
"""
|
||||
@spec delete_env(app, key, [timeout: timeout, persistent: boolean]) :: :ok
|
||||
def delete_env(app, key, opts \\ []) do
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` is started.
|
||||
|
||||
Same as `start/2` but returns `:ok` if the application was already
|
||||
started. This is useful in scripts and in test setup, where test
|
||||
applications need to be explicitly started:
|
||||
|
||||
:ok = Application.ensure_started(:my_test_dep)
|
||||
|
||||
"""
|
||||
@spec ensure_started(app, start_type) :: :ok | {:error, term}
|
||||
def ensure_started(app, type \\ :temporary) when is_atom(app) do
|
||||
:application.ensure_started(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` and its applications are started.
|
||||
|
||||
Same as `start/2` but also starts the applications listed under
|
||||
`:applications` in the `.app` file in case they were not previously
|
||||
started.
|
||||
"""
|
||||
@spec ensure_all_started(app, start_type) :: {:ok, [app]} | {:error, term}
|
||||
def ensure_all_started(app, type \\ :temporary) when is_atom(app) do
|
||||
:application.ensure_all_started(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts the given `app`.
|
||||
|
||||
If the `app` is not loaded, the application will first be loaded using `load/1`.
|
||||
Any included application, defined in the `:included_applications` key of the
|
||||
`.app` file will also be loaded, but they won't be started.
|
||||
|
||||
Furthermore, all applications listed in the `:applications` key must be explicitly
|
||||
started before this application is. If not, `{:error, {:not_started, app}}` is
|
||||
returned, where `app` is the name of the missing application.
|
||||
|
||||
In case you want to automatically load **and start** all of `app`'s dependencies,
|
||||
see `ensure_all_started/2`.
|
||||
|
||||
The `type` argument specifies the type of the application:
|
||||
|
||||
* `:permanent` - if `app` terminates, all other applications and the entire
|
||||
node are also terminated;
|
||||
* `:transient` - if `app` terminates with `:normal` reason, it is reported
|
||||
but no other applications are terminated. If a transient application terminates
|
||||
abnormally, all other applications and the entire node are also terminated;
|
||||
* `:temporary` - if `app` termiantes, it is reported but no other applications
|
||||
are terminated (the default);
|
||||
|
||||
Note that it is always possible to stop an application explicitly by calling
|
||||
`stop/1`. Regardless of the type of the application, no other applications will
|
||||
be affected.
|
||||
|
||||
Note also that the `:transient` type is of little practical use, since when a
|
||||
supervision tree terminates, the reason is set to `:shutdown`, not `:normal`.
|
||||
"""
|
||||
@spec start(app, start_type) :: :ok | {:error, term}
|
||||
def start(app, type \\ :temporary) when is_atom(app) do
|
||||
:application.start(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the given `app`.
|
||||
|
||||
When stopped, the application is still loaded.
|
||||
"""
|
||||
@spec stop(app) :: :ok | {:error, term}
|
||||
def stop(app) do
|
||||
:application.stop(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads the given `app`.
|
||||
|
||||
In order to be loaded, an `.app` file must be in the load paths.
|
||||
All `:included_applications` will also be loaded.
|
||||
|
||||
Loading the application does not start it nor load its modules, but
|
||||
it does load its environment.
|
||||
"""
|
||||
@spec load(app) :: :ok | {:error, term}
|
||||
def load(app) when is_atom(app) do
|
||||
:application.load(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unloads the given `app`.
|
||||
|
||||
It will also unload all `:included_applications`.
|
||||
Note that the function does not purge the application modules.
|
||||
"""
|
||||
@spec unload(app) :: :ok | {:error, term}
|
||||
def unload(app) when is_atom(app) do
|
||||
:application.unload(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the directory for app.
|
||||
|
||||
This information is returned based on the code path. Here is an
|
||||
example:
|
||||
|
||||
File.mkdir_p!("foo/ebin")
|
||||
Code.prepend_path("foo/ebin")
|
||||
Application.app_dir(:foo)
|
||||
#=> "foo"
|
||||
|
||||
Even though the directory is empty and there is no `.app` file
|
||||
it is considered the application directory based on the name
|
||||
"foo/ebin". The name may contain a dash `-` which is considered
|
||||
to be the app version and it is removed for the lookup purposes:
|
||||
|
||||
File.mkdir_p!("bar-123/ebin")
|
||||
Code.prepend_path("bar-123/ebin")
|
||||
Application.app_dir(:bar)
|
||||
#=> "bar-123"
|
||||
|
||||
For more information on code paths, check the `Code` module in
|
||||
Elixir and also Erlang's `:code` module.
|
||||
"""
|
||||
@spec app_dir(app) :: String.t
|
||||
def app_dir(app) when is_atom(app) do
|
||||
case :code.lib_dir(app) do
|
||||
lib when is_list(lib) -> String.from_char_data!(lib)
|
||||
{:error, :bad_name} -> raise ArgumentError, message: "unknown application: #{inspect app}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the given path inside `app_dir/1`.
|
||||
"""
|
||||
@spec app_dir(app, String.t) :: String.t
|
||||
def app_dir(app, path) when is_binary(path) do
|
||||
Path.join(app_dir(app), path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the error reason returned by `start/2`,
|
||||
`ensure_started/2, `stop/1`, `load/1` and `unload/1`,
|
||||
returns a string.
|
||||
"""
|
||||
@spec format_error(any) :: String.t
|
||||
def format_error(reason) do
|
||||
try do
|
||||
impl_format_error(reason)
|
||||
catch
|
||||
# A user could create an error that looks like a builtin one
|
||||
# causing an error.
|
||||
:error, _ ->
|
||||
inspect(reason)
|
||||
end
|
||||
end
|
||||
|
||||
# exit(:normal) call is special cased, undo the special case.
|
||||
defp impl_format_error({{:EXIT, :normal}, {mod, :start, args}}) do
|
||||
Exception.format_exit({:normal, {mod, :start, args}})
|
||||
end
|
||||
|
||||
# {:error, reason} return value
|
||||
defp impl_format_error({reason, {mod, :start, args}}) do
|
||||
Exception.format_mfa(mod, :start, args) <> " returned an error: " <>
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
|
||||
# error or exit(reason) call, use exit reason as reason.
|
||||
defp impl_format_error({:bad_return, {{mod, :start, args}, {:EXIT, reason}}}) do
|
||||
Exception.format_exit({reason, {mod, :start, args}})
|
||||
end
|
||||
|
||||
# bad return value
|
||||
defp impl_format_error({:bad_return, {{mod, :start, args}, return}}) do
|
||||
Exception.format_mfa(mod, :start, args) <>
|
||||
" returned a bad value: " <> inspect(return)
|
||||
end
|
||||
|
||||
defp impl_format_error({:already_started, app}) when is_atom(app) do
|
||||
"already started application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:not_started, app}) when is_atom(app) do
|
||||
"not started application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:bad_application, app}) do
|
||||
"bad application: #{inspect(app)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:already_loaded, app}) when is_atom(app) do
|
||||
"already loaded application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:not_loaded, app}) when is_atom(app) do
|
||||
"not loaded application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:invalid_restart_type, restart}) do
|
||||
"invalid application restart type: #{inspect(restart)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:invalid_name, name}) do
|
||||
"invalid application name: #{inspect(name)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:invalid_options, opts}) do
|
||||
"invalid application options: #{inspect(opts)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:badstartspec, spec}) do
|
||||
"bad application start specs: #{inspect(spec)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({'no such file or directory', file}) do
|
||||
"could not find application file: #{file}"
|
||||
end
|
||||
|
||||
defp impl_format_error(reason) do
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
defmodule Application.Behaviour do
|
||||
@moduledoc false
|
||||
|
||||
defmacro __using__(_) do
|
||||
# IO.write :stderr, "use Application.Behaviour is deprecated, please use Application instead\n#{Exception.format_stacktrace}"
|
||||
quote location: :keep do
|
||||
@behaviour :application
|
||||
|
||||
@doc false
|
||||
def stop(_state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defoverridable [stop: 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,464 +0,0 @@
|
||||
defmodule Base do
|
||||
import Bitwise
|
||||
|
||||
@moduledoc """
|
||||
This module provides data encoding and decoding functions
|
||||
according to [RFC 4648](http://tools.ietf.org/html/rfc4648).
|
||||
|
||||
This document defines the commonly used base 64, base 32, and base
|
||||
16 encoding schemes.
|
||||
"""
|
||||
|
||||
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'
|
||||
|
||||
Enum.each [ {:enc16, :dec16, b16_alphabet},
|
||||
{:enc64, :dec64, b64_alphabet},
|
||||
{:enc32, :dec32, b32_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, message: "non-alphabet digit found: #{<<c>>}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 16 encoded string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.encode16("foobar")
|
||||
"666F6F626172"
|
||||
|
||||
"""
|
||||
@spec encode16(binary) :: binary
|
||||
def encode16(data) when is_binary(data) do
|
||||
do_encode16(data, &enc16/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 16 encoded string into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| 0| 4| 4| 8| 8| 12| C|
|
||||
| 1| 1| 5| 5| 9| 9| 13| D|
|
||||
| 2| 2| 6| 6| 10| A| 14| E|
|
||||
| 3| 3| 7| 7| 11| B| 15| F|
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode16("666F6F626172")
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec decode16(binary) :: {:ok, binary} | :error
|
||||
def decode16(string) when is_binary(string) do
|
||||
{:ok, decode16!(string)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 16 encoded string into a binary string.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode16!("666F6F626172")
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec decode16!(binary) :: binary
|
||||
def decode16!(string) when is_binary(string) do
|
||||
do_decode16(string, &dec16/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 64 encoded string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.encode64("foobar")
|
||||
"Zm9vYmFy"
|
||||
|
||||
"""
|
||||
@spec encode64(binary) :: binary
|
||||
def encode64(data) when is_binary(data) do
|
||||
do_encode64(data, &enc64/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 17| R| 34| i| 51| z|
|
||||
| 1| B| 18| S| 35| j| 52| 0|
|
||||
| 2| C| 19| T| 36| k| 53| 1|
|
||||
| 3| D| 20| U| 37| l| 54| 2|
|
||||
| 4| E| 21| V| 38| m| 55| 3|
|
||||
| 5| F| 22| W| 39| n| 56| 4|
|
||||
| 6| G| 23| X| 40| o| 57| 5|
|
||||
| 7| H| 24| Y| 41| p| 58| 6|
|
||||
| 8| I| 25| Z| 42| q| 59| 7|
|
||||
| 9| J| 26| a| 43| r| 60| 8|
|
||||
| 10| K| 27| b| 44| s| 61| 9|
|
||||
| 11| L| 28| c| 45| t| 62| +|
|
||||
| 12| M| 29| d| 46| u| 63| /|
|
||||
| 13| N| 30| e| 47| v| | |
|
||||
| 14| O| 31| f| 48| w| (pad)| =|
|
||||
| 15| P| 32| g| 49| x| | |
|
||||
| 16| Q| 33| h| 50| y| | |
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode64("Zm9vYmFy")
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec decode64(binary) :: {:ok, binary} | :error
|
||||
def decode64(string) when is_binary(string) do
|
||||
{:ok, do_decode64(string, &dec64/1)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode64!("Zm9vYmFy")
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec decode64!(binary) :: binary
|
||||
def decode64!(string) when is_binary(string) do
|
||||
do_decode64(string, &dec64/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 64 encoded string with URL and filename
|
||||
safe alphabet.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.url_encode64(<<255,127,254,252>>)
|
||||
"_3_-_A=="
|
||||
|
||||
"""
|
||||
@spec url_encode64(binary) :: binary
|
||||
def url_encode64(data) when is_binary(data) do
|
||||
do_encode64(data, &enc64url/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string with URL and filename safe alphabet
|
||||
into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 17| R| 34| i| 51| z|
|
||||
| 1| B| 18| S| 35| j| 52| 0|
|
||||
| 2| C| 19| T| 36| k| 53| 1|
|
||||
| 3| D| 20| U| 37| l| 54| 2|
|
||||
| 4| E| 21| V| 38| m| 55| 3|
|
||||
| 5| F| 22| W| 39| n| 56| 4|
|
||||
| 6| G| 23| X| 40| o| 57| 5|
|
||||
| 7| H| 24| Y| 41| p| 58| 6|
|
||||
| 8| I| 25| Z| 42| q| 59| 7|
|
||||
| 9| J| 26| a| 43| r| 60| 8|
|
||||
| 10| K| 27| b| 44| s| 61| 9|
|
||||
| 11| L| 28| c| 45| t| 62| -|
|
||||
| 12| M| 29| d| 46| u| 63| _|
|
||||
| 13| N| 30| e| 47| v| | |
|
||||
| 14| O| 31| f| 48| w| (pad)| =|
|
||||
| 15| P| 32| g| 49| x| | |
|
||||
| 16| Q| 33| h| 50| y| | |
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.url_decode64("_3_-_A==")
|
||||
{:ok, <<255,127,254,252>>}
|
||||
|
||||
"""
|
||||
@spec url_decode64(binary) :: {:ok, binary} | :error
|
||||
def url_decode64(string) when is_binary(string) do
|
||||
{:ok, do_decode64(string, &dec64url/1)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string with URL and filename safe alphabet
|
||||
into a binary string.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.url_decode64!("_3_-_A==")
|
||||
<<255,127,254,252>>
|
||||
|
||||
"""
|
||||
@spec url_decode64!(binary) :: binary
|
||||
def url_decode64!(string) when is_binary(string) do
|
||||
do_decode64(string, &dec64url/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 32 encoded string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.encode32("foobar")
|
||||
"MZXW6YTBOI======"
|
||||
|
||||
"""
|
||||
@spec encode32(binary) :: binary
|
||||
def encode32(data) when is_binary(data) do
|
||||
do_encode32(data, &enc32/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 9| J| 18| S| 27| 3|
|
||||
| 1| B| 10| K| 19| T| 28| 4|
|
||||
| 2| C| 11| L| 20| U| 29| 5|
|
||||
| 3| D| 12| M| 21| V| 30| 6|
|
||||
| 4| E| 13| N| 22| W| 31| 7|
|
||||
| 5| F| 14| O| 23| X| | |
|
||||
| 6| G| 15| P| 24| Y| (pad)| =|
|
||||
| 7| H| 16| Q| 25| Z| | |
|
||||
| 8| I| 17| R| 26| 2| | |
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode32("MZXW6YTBOI======")
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec decode32(binary) :: {:ok, binary} | :error
|
||||
def decode32(string) do
|
||||
{:ok, do_decode32(string, &dec32/1)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string into a binary string.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode32!("MZXW6YTBOI======")
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec decode32!(binary) :: binary
|
||||
def decode32!(string) do
|
||||
do_decode32(string, &dec32/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 32 encoded string with an
|
||||
extended hexadecimal alphabet.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.hex_encode32("foobar")
|
||||
"CPNMUOJ1E8======"
|
||||
|
||||
"""
|
||||
@spec hex_encode32(binary) :: binary
|
||||
def hex_encode32(data) when is_binary(data) do
|
||||
do_encode32(data, &enc32hex/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string with extended hexadecimal alphabet
|
||||
into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| 0| 9| 9| 18| I| 27| R|
|
||||
| 1| 1| 10| A| 19| J| 28| S|
|
||||
| 2| 2| 11| B| 20| K| 29| T|
|
||||
| 3| 3| 12| C| 21| L| 30| U|
|
||||
| 4| 4| 13| D| 22| M| 31| V|
|
||||
| 5| 5| 14| E| 23| N| | |
|
||||
| 6| 6| 15| F| 24| O| (pad)| =|
|
||||
| 7| 7| 16| G| 25| P| | |
|
||||
| 8| 8| 17| H| 26| Q| | |
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.hex_decode32("CPNMUOJ1E8======")
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec hex_decode32(binary) :: {:ok, binary} | :error
|
||||
def hex_decode32(string) when is_binary(string) do
|
||||
{:ok, do_decode32(string, &dec32hex/1)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string with extended hexadecimal alphabet
|
||||
into a binary string.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.hex_decode32!("CPNMUOJ1E8======")
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec hex_decode32!(binary) :: binary
|
||||
def hex_decode32!(string) when is_binary(string) do
|
||||
do_decode32(string, &dec32hex/1)
|
||||
end
|
||||
|
||||
defp do_encode16(<<>>, _), do: <<>>
|
||||
defp do_encode16(data, enc) do
|
||||
for <<c::4 <- data>>, into: <<>>, do: <<enc.(c)::8>>
|
||||
end
|
||||
|
||||
defp do_decode16(<<>>, _), do: <<>>
|
||||
defp do_decode16(string, dec) when rem(byte_size(string), 2) == 0 do
|
||||
for <<c1::8, c2::8 <- string>>, into: <<>> do
|
||||
<<dec.(c1)::4, dec.(c2)::4>>
|
||||
end
|
||||
end
|
||||
defp do_decode16(_, _) do
|
||||
raise ArgumentError, message: "odd-length string"
|
||||
end
|
||||
|
||||
defp do_encode64(<<>>, _), do: <<>>
|
||||
defp do_encode64(data, enc) do
|
||||
split = 3 * div(byte_size(data), 3)
|
||||
<<main::[size(split), binary], rest::binary>> = data
|
||||
main = for <<c::6 <- main>>, into: <<>>, do: <<enc.(c)::8>>
|
||||
case rest do
|
||||
<<c1::6, c2::6, c3::4>> ->
|
||||
<<main::binary, enc.(c1)::8, enc.(c2)::8, enc.(bsl(c3, 2))::8, ?=>>
|
||||
<<c1::6, c2::2>> ->
|
||||
<<main::binary, enc.(c1)::8, enc.(bsl(c2, 4))::8, ?=, ?=>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decode64(<<>>, _), do: <<>>
|
||||
defp do_decode64(string, dec) 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: <<dec.(c)::6>>
|
||||
case rest do
|
||||
<<c1::8, c2::8, ?=, ?=>> ->
|
||||
<<main::binary, dec.(c1)::6, bsr(dec.(c2), 4)::2>>
|
||||
<<c1::8, c2::8, c3::8, ?=>> ->
|
||||
<<main::binary, dec.(c1)::6, dec.(c2)::6, bsr(dec.(c3), 2)::4>>
|
||||
<<c1::8, c2::8, c3::8, c4::8>> ->
|
||||
<<main::binary, dec.(c1)::6, dec.(c2)::6, dec.(c3)::6, dec.(c4)::6>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
defp do_decode64(_, _) do
|
||||
raise ArgumentError, message: "incorrect padding"
|
||||
end
|
||||
|
||||
defp do_encode32(<<>>, _), do: <<>>
|
||||
defp do_encode32(data, enc) do
|
||||
split = 5 * div(byte_size(data), 5)
|
||||
<<main::[size(split), binary], rest::binary>> = data
|
||||
main = for <<c::5 <- main>>, into: <<>>, do: <<enc.(c)::8>>
|
||||
case rest do
|
||||
<<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(c4)::8,
|
||||
enc.(c5)::8, enc.(c6)::8, enc.(bsl(c7, 3))::8, ?=>>
|
||||
<<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(c4)::8,
|
||||
enc.(bsl(c5, 1))::8, ?=, ?=, ?=>>
|
||||
<<c1::5, c2::5, c3::5, c4::1>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(bsl(c4, 4))::8,
|
||||
?=, ?=, ?=, ?=>>
|
||||
<<c1::5, c2::3>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(bsl(c2, 2))::8, ?=, ?=,
|
||||
?=, ?=, ?=, ?=>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decode32(<<>>, _), do: <<>>
|
||||
defp do_decode32(string, dec) 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: <<dec.(c)::5>>
|
||||
case rest do
|
||||
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
|
||||
<<main::binary, dec.(c1)::5, bsr(dec.(c2), 2)::3>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, bsr(dec.(c4), 4)::1>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
|
||||
bsr(dec.(c5), 1)::4>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
|
||||
dec.(c5)::5, dec.(c6)::5, bsr(dec.(c7), 3)::2>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
|
||||
dec.(c5)::5, dec.(c6)::5, dec.(c7)::5, dec.(c8)::5>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
defp do_decode32(_, _) do
|
||||
raise ArgumentError, message: "incorrect padding"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,152 +0,0 @@
|
||||
defmodule Behaviour do
|
||||
@moduledoc """
|
||||
Utilities for defining behaviour interfaces.
|
||||
|
||||
Behaviours can be referenced by other modules
|
||||
to ensure they implement required callbacks.
|
||||
|
||||
For example, you can specify the `URI.Parser`
|
||||
behaviour as follows:
|
||||
|
||||
defmodule URI.Parser do
|
||||
use Behaviour
|
||||
|
||||
@doc "Parses the given URL"
|
||||
defcallback parse(uri_info :: URI.t) :: URI.t
|
||||
|
||||
@doc "Defines a default port"
|
||||
defcallback default_port() :: integer
|
||||
end
|
||||
|
||||
And then a module may use it as:
|
||||
|
||||
defmodule URI.HTTP do
|
||||
@behaviour URI.Parser
|
||||
def default_port(), do: 80
|
||||
def parse(info), do: info
|
||||
end
|
||||
|
||||
If the behaviour changes or `URI.HTTP` does
|
||||
not implement one of the callbacks, a warning
|
||||
will be raised.
|
||||
|
||||
## Implementation
|
||||
|
||||
Since Erlang R15, behaviours must be defined via
|
||||
`@callback` attributes. `defcallback` is a simple
|
||||
mechanism that defines the `@callback` attribute
|
||||
according to the given type specification. `defcallback` allows
|
||||
documentation to be created for the callback and defines
|
||||
a custom function signature.
|
||||
|
||||
The callbacks and their documentation can be retrieved
|
||||
via the `__behaviour__` callback function.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Define a function callback according to the given type specification.
|
||||
"""
|
||||
defmacro defcallback(spec) do
|
||||
do_defcallback(split_spec(spec, quote(do: term)), __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Define a macro callback according to the given type specification.
|
||||
"""
|
||||
defmacro defmacrocallback(spec) do
|
||||
do_defmacrocallback(split_spec(spec, quote(do: Macro.t)), __CALLER__)
|
||||
end
|
||||
|
||||
defp split_spec({:when, _, [{:::, _, [spec, return]}, guard]}, _default) do
|
||||
{spec, return, guard}
|
||||
end
|
||||
|
||||
defp split_spec({:when, _, [spec, guard]}, default) do
|
||||
{spec, default, guard}
|
||||
end
|
||||
|
||||
defp split_spec({:::, _, [spec, return]}, _default) do
|
||||
{spec, return, []}
|
||||
end
|
||||
|
||||
defp split_spec(spec, default) do
|
||||
{spec, default, []}
|
||||
end
|
||||
|
||||
defp do_defcallback({spec, return, guards}, caller) do
|
||||
case Macro.decompose_call(spec) do
|
||||
{name, args} ->
|
||||
do_callback(:def, name, args, name, length(args), args, return, guards, caller)
|
||||
_ ->
|
||||
raise ArgumentError, message: "invalid syntax in defcallback #{Macro.to_string(spec)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_defmacrocallback({spec, return, guards}, caller) do
|
||||
case Macro.decompose_call(spec) do
|
||||
{name, args} ->
|
||||
do_callback(:defmacro, :"MACRO-#{name}", [quote(do: env :: Macro.Env.t)|args],
|
||||
name, length(args), args, return, guards, caller)
|
||||
_ ->
|
||||
raise ArgumentError, message: "invalid syntax in defmacrocallback #{Macro.to_string(spec)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_callback(kind, name, args, docs_name, docs_arity, _docs_args, return, guards, caller) do
|
||||
Enum.each args, fn
|
||||
{:::, _, [left, right]} ->
|
||||
ensure_not_default(left)
|
||||
ensure_not_default(right)
|
||||
left
|
||||
other ->
|
||||
ensure_not_default(other)
|
||||
other
|
||||
end
|
||||
|
||||
quote do
|
||||
@callback unquote(name)(unquote_splicing(args)) :: unquote(return) when unquote(guards)
|
||||
Behaviour.store_docs(__MODULE__, unquote(caller.line), unquote(kind),
|
||||
unquote(docs_name), unquote(docs_arity))
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_not_default({:\\, _, [_, _]}) do
|
||||
raise ArgumentError, message: "default arguments \\\\ not supported in defcallback/defmacrocallback"
|
||||
end
|
||||
|
||||
defp ensure_not_default(_), do: :ok
|
||||
|
||||
@doc false
|
||||
def store_docs(module, line, kind, name, arity) do
|
||||
doc = Module.get_attribute module, :doc
|
||||
Module.delete_attribute module, :doc
|
||||
Module.put_attribute module, :behaviour_docs, {{name, arity}, line, kind, doc}
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
Module.register_attribute(__MODULE__, :behaviour_docs, accumulate: true)
|
||||
@before_compile unquote(__MODULE__)
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __before_compile__(env) do
|
||||
docs = if Code.compiler_options[:docs] do
|
||||
Enum.reverse Module.get_attribute(env.module, :behaviour_docs)
|
||||
end
|
||||
|
||||
quote do
|
||||
@doc false
|
||||
def __behaviour__(:callbacks) do
|
||||
__MODULE__.behaviour_info(:callbacks)
|
||||
end
|
||||
|
||||
def __behaviour__(:docs) do
|
||||
unquote(Macro.escape(docs))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
defmodule Bitwise do
|
||||
@moduledoc """
|
||||
This module provides macros and operators for bitwise operators.
|
||||
These macros can be used in guards.
|
||||
|
||||
The easiest way to use is to simply import them into
|
||||
your module:
|
||||
|
||||
iex> use Bitwise
|
||||
iex> bnot 1
|
||||
-2
|
||||
iex> 1 &&& 1
|
||||
1
|
||||
|
||||
You can select to include only or skip operators by passing options:
|
||||
|
||||
iex> use Bitwise, only_operators: true
|
||||
iex> 1 &&& 1
|
||||
1
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Allow a developer to use this module in their programs with
|
||||
the following options:
|
||||
|
||||
* `:only_operators` - Include only operators;
|
||||
* `:skip_operators` - Skip operators;
|
||||
|
||||
"""
|
||||
defmacro __using__(options) do
|
||||
except = cond do
|
||||
Keyword.get(options, :only_operators) ->
|
||||
[bnot: 1, band: 2, bor: 2, bxor: 2, bsl: 2, bsr: 2]
|
||||
Keyword.get(options, :skip_operators) ->
|
||||
[~~~: 1, &&&: 2, |||: 2, ^^^: 2, <<<: 2, >>>: 2]
|
||||
true -> []
|
||||
end
|
||||
|
||||
quote do
|
||||
import Bitwise, except: unquote(except)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise not.
|
||||
"""
|
||||
defmacro bnot(expr) do
|
||||
quote do: :erlang.bnot(unquote(expr))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise not as operator.
|
||||
"""
|
||||
defmacro ~~~expr do
|
||||
quote do: :erlang.bnot(unquote(expr))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise and.
|
||||
"""
|
||||
defmacro band(left, right) do
|
||||
quote do: :erlang.band(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise and as operator.
|
||||
"""
|
||||
defmacro left &&& right do
|
||||
quote do: :erlang.band(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise or.
|
||||
"""
|
||||
defmacro bor(left, right) do
|
||||
quote do: :erlang.bor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise or as operator.
|
||||
"""
|
||||
defmacro left ||| right do
|
||||
quote do: :erlang.bor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise xor.
|
||||
"""
|
||||
defmacro bxor(left, right) do
|
||||
quote do: :erlang.bxor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise xor as operator.
|
||||
"""
|
||||
defmacro left ^^^ right do
|
||||
quote do: :erlang.bxor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift left.
|
||||
"""
|
||||
defmacro bsl(left, right) do
|
||||
quote do: :erlang.bsl(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift left as operator.
|
||||
"""
|
||||
defmacro left <<< right do
|
||||
quote do: :erlang.bsl(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift right.
|
||||
"""
|
||||
defmacro bsr(left, right) do
|
||||
quote do: :erlang.bsr(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift right as operator.
|
||||
"""
|
||||
defmacro left >>> right do
|
||||
quote do: :erlang.bsr(unquote(left), unquote(right))
|
||||
end
|
||||
end
|
||||
@@ -1,476 +0,0 @@
|
||||
defmodule Code do
|
||||
defexception LoadError, [:file, :message] do
|
||||
def exception(opts) do
|
||||
file = opts[:file]
|
||||
LoadError[message: "could not load #{file}", file: file]
|
||||
end
|
||||
end
|
||||
|
||||
@moduledoc """
|
||||
Utilities for managing code compilation, code evaluation and code loading.
|
||||
|
||||
This module complements [Erlang's code module](http://www.erlang.org/doc/man/code.html)
|
||||
to add behaviour which is specific to Elixir.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
List all loaded files.
|
||||
"""
|
||||
def loaded_files do
|
||||
:elixir_code_server.call :loaded
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove files from the loaded files list.
|
||||
|
||||
The modules defined in the file are not removed;
|
||||
calling this function only removes them from the list,
|
||||
allowing them to be required again.
|
||||
"""
|
||||
def unload_files(files) do
|
||||
:elixir_code_server.cast {:unload_files, files}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Append a path to the Erlang VM code path.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being appended.
|
||||
"""
|
||||
def append_path(path) do
|
||||
:code.add_pathz(to_char_list(Path.expand path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepend a path to the Erlang VM code path.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being prepended.
|
||||
"""
|
||||
def prepend_path(path) do
|
||||
:code.add_patha(to_char_list(Path.expand path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a path from the Erlang VM code path.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being deleted.
|
||||
"""
|
||||
def delete_path(path) do
|
||||
:code.del_path(to_char_list(Path.expand path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluate the contents given by `string`.
|
||||
|
||||
The `binding` argument is a keyword list of variable bindings.
|
||||
The `opts` argument is a keyword list of environment options.
|
||||
|
||||
Those options can be:
|
||||
|
||||
* `:file` - the file to be considered in the evaluation
|
||||
* `:line` - the line on which the script starts
|
||||
* `:delegate_locals_to` - delegate local calls to the given module,
|
||||
the default is to not delegate
|
||||
|
||||
Additionally, the following scope values can be configured:
|
||||
|
||||
* `:aliases` - a list of tuples with the alias and its target
|
||||
* `:requires` - a list of modules required
|
||||
* `:functions` - a list of tuples where the first element is a module
|
||||
and the second a list of imported function names and arity. The list
|
||||
of function names and arity must be sorted
|
||||
* `:macros` - a list of tuples where the first element is a module
|
||||
and the second a list of imported macro names and arity. The list
|
||||
of function names and arity must be sorted
|
||||
|
||||
Notice that setting any of the values above overrides Elixir's default
|
||||
values. For example, setting `:requires` to `[]`, will no longer
|
||||
automatically require the `Kernel` module; in the same way setting
|
||||
`:macros` will no longer auto-import `Kernel` macros like `if`, `case`,
|
||||
etc.
|
||||
|
||||
Returns a tuple of the form `{value, binding}`,
|
||||
where `value` is the value returned from evaluating `string`.
|
||||
If an error occurs while evaluating `string` an exception will be raised.
|
||||
|
||||
`binding` is a keyword list with the value of all variable bindings
|
||||
after evaluating `string`. The binding key is usually an atom, but it
|
||||
may be a tuple for variables defined in a different context.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
iex> Code.eval_string("c = a + b", [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2, c: 3]}
|
||||
|
||||
iex> Code.eval_string("a = a + b", [a: 1, b: 2])
|
||||
{3, [a: 3, b: 2]}
|
||||
|
||||
For convenience, you can pass `__ENV__` as the `opts` argument and
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
iex> Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
{value, binding, _env, _scope} = :elixir.eval to_char_list(string), binding, Map.to_list(env)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
def eval_string(string, binding, opts) when is_list(opts) do
|
||||
validate_eval_opts(opts)
|
||||
{value, binding, _env, _scope} = :elixir.eval to_char_list(string), binding, opts
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluate the quoted contents.
|
||||
|
||||
See `eval_string/3` for a description of arguments and return values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> contents = quote(hygiene: [vars: false], do: a + b)
|
||||
iex> Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
For convenience, you can pass `__ENV__` as the `opts` argument and
|
||||
all options will be automatically extracted from the current environment:
|
||||
|
||||
iex> contents = quote(hygiene: [vars: false], do: a + b)
|
||||
iex> Code.eval_quoted(contents, [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
def eval_quoted(quoted, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
|
||||
{value, binding, _env, _scope} = :elixir.eval_quoted quoted, binding, Map.to_list(env)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
||||
validate_eval_opts(opts)
|
||||
{value, binding, _env, _scope} = :elixir.eval_quoted quoted, binding, opts
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
defp validate_eval_opts(opts) do
|
||||
if f = opts[:functions], do: validate_imports(:functions, f)
|
||||
if m = opts[:macros], do: validate_imports(:macros, m)
|
||||
if a = opts[:aliases], do: validate_aliases(:aliases, a)
|
||||
if r = opts[:requires], do: validate_requires(:requires, r)
|
||||
end
|
||||
|
||||
defp validate_requires(kind, requires) do
|
||||
valid = is_list(requires) and Enum.all?(requires, &is_atom(&1))
|
||||
|
||||
unless valid do
|
||||
raise ArgumentError, message: "expected :#{kind} option given to eval in the format: [module]"
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_aliases(kind, aliases) do
|
||||
valid = is_list(aliases) and Enum.all?(aliases, fn {k, v} ->
|
||||
is_atom(k) and is_atom(v)
|
||||
end)
|
||||
|
||||
unless valid do
|
||||
raise ArgumentError, message: "expected :#{kind} option given to eval in the format: [{module, module}]"
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_imports(kind, imports) do
|
||||
valid = is_list(imports) and Enum.all?(imports, fn {k, v} ->
|
||||
is_atom(k) and is_list(v) and Enum.all?(v, fn {name, arity} ->
|
||||
is_atom(name) and is_integer(arity)
|
||||
end)
|
||||
end)
|
||||
|
||||
unless valid do
|
||||
raise ArgumentError, message: "expected :#{kind} option given to eval in the format: [{module, [{name, arity}]}]"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert the given string to its quoted form.
|
||||
|
||||
Returns `{:ok, quoted_form}`
|
||||
if it succeeds, `{:error, {line, error, token}}` otherwise.
|
||||
|
||||
## Options
|
||||
|
||||
* `:file` - The filename to be used in stacktraces
|
||||
and the file reported in the `__ENV__` variable.
|
||||
|
||||
* `:line` - The line reported in the `__ENV__` variable.
|
||||
|
||||
* `:existing_atoms_only` - When `true`, raises an error
|
||||
when non-existing atoms are found by the tokenizer.
|
||||
|
||||
## Macro.to_string/2
|
||||
|
||||
The opposite of converting a string to its quoted form is
|
||||
`Macro.to_string/2`, which converts a quoted form to a string/binary
|
||||
representation.
|
||||
"""
|
||||
def string_to_quoted(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get opts, :file, "nofile"
|
||||
line = Keyword.get opts, :line, 1
|
||||
:elixir.string_to_quoted(to_char_list(string), line, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert the given string to its quoted form.
|
||||
|
||||
It returns the ast if it succeeds,
|
||||
raises an exception otherwise. The exception is a `TokenMissingError`
|
||||
in case a token is missing (usually because the expression is incomplete),
|
||||
`SyntaxError` otherwise.
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get opts, :file, "nofile"
|
||||
line = Keyword.get opts, :line, 1
|
||||
:elixir.string_to_quoted!(to_char_list(string), line, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evals the given file.
|
||||
|
||||
Accepts `relative_to` as an argument to tell where the file is located.
|
||||
|
||||
While `load_file` loads a file and returns the loaded modules and their
|
||||
byte code, `eval_file` simply evalutes the file contents and returns the
|
||||
evaluation result and its bindings.
|
||||
"""
|
||||
def eval_file(file, relative_to \\ nil) do
|
||||
file = find_file(file, relative_to)
|
||||
eval_string File.read!(file), [], []
|
||||
end
|
||||
|
||||
@doc """
|
||||
Load the given file.
|
||||
|
||||
Accepts `relative_to` as an argument to tell where the file is located.
|
||||
If the file was already required/loaded, loads it again.
|
||||
|
||||
It returns a list of tuples `{ModuleName, <<byte_code>>}`, one tuple for
|
||||
each module defined in the file.
|
||||
|
||||
Notice that if `load_file` is invoked by different processes concurrently,
|
||||
the target file will be loaded concurrently many times. Check `require_file/2`
|
||||
if you don't want a file to be loaded concurrently.
|
||||
"""
|
||||
def load_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
file = find_file(file, relative_to)
|
||||
:elixir_code_server.call {:acquire, file}
|
||||
loaded = :elixir_compiler.file file
|
||||
:elixir_code_server.cast {:loaded, file}
|
||||
loaded
|
||||
end
|
||||
|
||||
@doc """
|
||||
Requires the given `file`.
|
||||
|
||||
Accepts `relative_to` as an argument to tell where the file is located.
|
||||
The return value is the same as that of `load_file/2`. If the file was already
|
||||
required/loaded, doesn't do anything and returns `nil`.
|
||||
|
||||
Notice that if `require_file` is invoked by different processes concurrently,
|
||||
the first process to invoke `require_file` acquires a lock and the remaining
|
||||
ones will block until the file is available. I.e. if `require_file` is called
|
||||
N times with a given file, it will be loaded only once. The first process to
|
||||
call `require_file` will get the list of loaded modules, others will get `nil`.
|
||||
|
||||
Check `load_file/2` if you want a file to be loaded multiple times.
|
||||
"""
|
||||
def require_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
file = find_file(file, relative_to)
|
||||
|
||||
case :elixir_code_server.call({:acquire, file}) do
|
||||
:loaded ->
|
||||
nil
|
||||
{:queued, ref} ->
|
||||
receive do {:elixir_code_server, ^ref, :loaded} -> nil end
|
||||
:proceed ->
|
||||
loaded = :elixir_compiler.file file
|
||||
:elixir_code_server.cast {:loaded, file}
|
||||
loaded
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the compilation options from the code server.
|
||||
|
||||
Check `compiler_options/1` for more information.
|
||||
"""
|
||||
def compiler_options do
|
||||
:elixir_code_server.call :compiler_options
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets compilation options.
|
||||
|
||||
These options are global since they are stored by Elixir's Code Server.
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retain documentation in the compiled module,
|
||||
`true` by default;
|
||||
|
||||
* `:debug_info` - when `true`, retain debug information in the compiled module.
|
||||
This allows a developer to reconstruct the original source
|
||||
code, `false` by default;
|
||||
|
||||
* `:ignore_module_conflict` - when `true`, override modules that were already defined
|
||||
without raising errors, `false` by default;
|
||||
|
||||
* `:warnings_as_errors` - cause compilation to fail when warnings are generated;
|
||||
|
||||
"""
|
||||
def compiler_options(opts) do
|
||||
:elixir_code_server.cast {:compiler_options, opts}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given string.
|
||||
|
||||
Returns a list of tuples where the first element is the module name
|
||||
and the second one is its byte code (as a binary).
|
||||
|
||||
For compiling many files at once, check `Kernel.ParallelCompiler.files/2`.
|
||||
"""
|
||||
def compile_string(string, file \\ "nofile") when is_binary(file) do
|
||||
:elixir_compiler.string to_char_list(string), file
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the quoted expression.
|
||||
|
||||
Returns a list of tuples where the first element is the module name and
|
||||
the second one is its byte code (as a binary).
|
||||
"""
|
||||
def compile_quoted(quoted, file \\ "nofile") when is_binary(file) do
|
||||
:elixir_compiler.quoted quoted, file
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is loaded.
|
||||
|
||||
If the module is already loaded, this works as no-op. If the module
|
||||
was not yet loaded, it tries to load it.
|
||||
|
||||
If it succeeds loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
## Code loading on the Erlang VM
|
||||
|
||||
Erlang has two modes to load code: interactive and embedded.
|
||||
|
||||
By default, the Erlang VM runs in interactive mode, where modules
|
||||
are loaded as needed. In embedded mode the opposite happens, as all
|
||||
modules need to be loaded upfront or explicitly.
|
||||
|
||||
Therefore, this function is used to check if a module is loaded
|
||||
before using it and allows one to react accordingly. For example, the `URI`
|
||||
module uses this function to check if a specific parser exists for a given
|
||||
URI scheme.
|
||||
|
||||
## `Code.ensure_compiled/1`
|
||||
|
||||
Elixir also contains an `ensure_compiled/1` function that is a
|
||||
superset of `ensure_loaded/1`.
|
||||
|
||||
Since Elixir's compilation happens in parallel, in some situations
|
||||
you may need to use a module that was not yet compiled, therefore
|
||||
it can't even be loaded.
|
||||
|
||||
`ensure_compiled/1` halts the current process until the
|
||||
module we are depending on is available.
|
||||
|
||||
In most cases, `ensure_loaded/1` is enough. `ensure_compiled/1`
|
||||
must be used in rare cases, usually involving macros that need to
|
||||
invoke a module for callback information.
|
||||
"""
|
||||
def ensure_loaded(module) when is_atom(module) do
|
||||
:code.ensure_loaded(module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is loaded.
|
||||
|
||||
Similar to `ensure_loaded/1`, but returns `true` if the module
|
||||
is already loaded or was successfully loaded. Returns `false`
|
||||
otherwise.
|
||||
"""
|
||||
def ensure_loaded?(module) do
|
||||
match?({:module, ^module}, ensure_loaded(module))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
If the module is already loaded, it works as no-op. If the module was
|
||||
not loaded yet, it checks if it needs to be compiled first and then
|
||||
tries to load it.
|
||||
|
||||
If it succeeds loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
Check `ensure_loaded/1` for more information on module loading
|
||||
and when to use `ensure_loaded/1` or `ensure_compiled/1`.
|
||||
"""
|
||||
def ensure_compiled(module) when is_atom(module) do
|
||||
case :code.ensure_loaded(module) do
|
||||
{:error, :nofile} = error ->
|
||||
case :erlang.get(:elixir_ensure_compiled) do
|
||||
:undefined -> error
|
||||
_ ->
|
||||
try do
|
||||
module.__info__(:module)
|
||||
{:module, module}
|
||||
rescue
|
||||
UndefinedFunctionError -> error
|
||||
end
|
||||
end
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
Similar to `ensure_compiled/1`, but returns `true` if the module
|
||||
is already loaded or was successfully loaded and compiled.
|
||||
Returns `false` otherwise.
|
||||
"""
|
||||
def ensure_compiled?(module) do
|
||||
match?({:module, ^module}, ensure_compiled(module))
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
# Finds the file given the relative_to path.
|
||||
#
|
||||
# If the file is found, returns its path in binary, fails otherwise.
|
||||
defp find_file(file, relative_to) do
|
||||
file = if relative_to do
|
||||
Path.expand(file, relative_to)
|
||||
else
|
||||
Path.expand(file)
|
||||
end
|
||||
|
||||
if File.regular?(file) do
|
||||
file
|
||||
else
|
||||
raise LoadError, file: file
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,117 +0,0 @@
|
||||
defprotocol Collectable do
|
||||
@moduledoc """
|
||||
A protocol to traverse data structures.
|
||||
|
||||
The `Enum.into/2` function uses this protocol to insert an
|
||||
enumerable into a collection:
|
||||
|
||||
iex> Enum.into([a: 1, b: 2], %{})
|
||||
%{a: 1, b: 2}
|
||||
|
||||
If a collection implements both `Enumerable` and `Collectable`, both
|
||||
operations can be combined with `Enum.traverse/2`:
|
||||
|
||||
iex> Enum.traverse(%{a: 1, b: 2}, fn {k, v} -> {k, v * 2} end)
|
||||
%{a: 2, b: 4}
|
||||
|
||||
## Why Collectable?
|
||||
|
||||
The `Enumerable` protocol is useful to take values out of a collection.
|
||||
In order to support a wide range of values, the functions provided by
|
||||
the `Enumerable` protocol do not keep shape. For example, passing a
|
||||
dictionary to `Enum.map/2` always returns a list.
|
||||
|
||||
This design is intentional. `Enumerable` was designed to support infinite
|
||||
collections, resources and other structures with fixed shape. For example,
|
||||
it doesn't make sense to insert values into a range, as it has a fixed
|
||||
shape where just the range limits are stored.
|
||||
|
||||
The `Collectable` module was designed to fill the gap left by the
|
||||
`Enumerable` protocol. It provides two functions: `into/1` and `empty/1`.
|
||||
|
||||
`into/1` can be seen as the opposite of `Enumerable.reduce/3`. If
|
||||
`Enumerable` is about taking values out, `Collectable.into/1` is about
|
||||
collecting those values into a structure.
|
||||
|
||||
`empty/1` receives a collectable and returns an empty version of the
|
||||
same collectable. By combining the enumerable functionality with `into/1`
|
||||
and `empty/1`, one can, for example, implement a traversal mechanism.
|
||||
"""
|
||||
|
||||
@type command :: {:cont, term} | :done | :halt
|
||||
|
||||
@doc """
|
||||
Receives a collectable structure and returns an empty one.
|
||||
"""
|
||||
@spec empty(t) :: t
|
||||
def empty(collectable)
|
||||
|
||||
@doc """
|
||||
Returns a function that collects values alongside
|
||||
the initial accumulation value.
|
||||
|
||||
The returned function receives a collectable and injects a given
|
||||
value into it for every `{:cont, term}` instruction.
|
||||
|
||||
`:done` is passed when no further values will be injected, useful
|
||||
for closing resources and normalizing values. A collectable must
|
||||
be returned on `:done`.
|
||||
|
||||
If injection is suddenly interrupted, `:halt` is passed and it can
|
||||
return any value, as it won't be used.
|
||||
"""
|
||||
@spec into(t) :: {term, (term, command -> t | term)}
|
||||
def into(collectable)
|
||||
end
|
||||
|
||||
defimpl Collectable, for: List do
|
||||
def empty(_list) do
|
||||
[]
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{[], fn
|
||||
list, {:cont, x} -> [x|list]
|
||||
list, :done -> original ++ :lists.reverse(list)
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: BitString do
|
||||
def empty(_bitstring) do
|
||||
""
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{original, fn
|
||||
bitstring, {:cont, x} -> <<bitstring :: bits, x :: bits>>
|
||||
bitstring, :done -> bitstring
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Function do
|
||||
def empty(function) do
|
||||
function
|
||||
end
|
||||
|
||||
def into(function) do
|
||||
{function, function}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Map do
|
||||
def empty(_map) do
|
||||
%{}
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{original, fn
|
||||
map, {:cont, {k, v}} -> :maps.put(k, v, map)
|
||||
map, :done -> map
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
@@ -1,492 +0,0 @@
|
||||
defmodule Dict do
|
||||
@moduledoc ~S"""
|
||||
This module specifies the Dict API expected to be
|
||||
implemented by different dictionaries. It also provides
|
||||
functions that redirect to the underlying Dict, allowing
|
||||
a developer to work with different Dict implementations
|
||||
using one API.
|
||||
|
||||
To create a new dict, use the `new` functions defined
|
||||
by each dict type:
|
||||
|
||||
HashDict.new #=> creates an empty HashDict
|
||||
|
||||
In the examples below, `dict_impl` means a specific
|
||||
`Dict` implementation, for example `HashDict` or `Map`.
|
||||
|
||||
## Protocols
|
||||
|
||||
Besides implementing the functions in this module, all
|
||||
dictionaries are required to implement the `Access`
|
||||
protocol:
|
||||
|
||||
iex> dict = dict_impl.new
|
||||
iex> dict = Dict.put(dict, :hello, :world)
|
||||
iex> dict[:hello]
|
||||
:world
|
||||
|
||||
As well as the `Enumerable` and `Collectable` protocols.
|
||||
|
||||
## Match
|
||||
|
||||
Dictionaries are required to implement all operations
|
||||
using the match (`===`) operator.
|
||||
"""
|
||||
|
||||
use Behaviour
|
||||
|
||||
@type key :: any
|
||||
@type value :: any
|
||||
@type t :: list | map
|
||||
|
||||
defcallback new :: t
|
||||
defcallback delete(t, key) :: t
|
||||
defcallback drop(t, Enum.t) :: t
|
||||
defcallback equal?(t, t) :: boolean
|
||||
defcallback get(t, key) :: value
|
||||
defcallback get(t, key, value) :: value
|
||||
defcallback fetch(t, key) :: {:ok, value} | :error
|
||||
defcallback fetch!(t, key) :: value | no_return
|
||||
defcallback has_key?(t, key) :: boolean
|
||||
defcallback keys(t) :: [key]
|
||||
defcallback merge(t, t) :: t
|
||||
defcallback merge(t, t, (key, value, value -> value)) :: t
|
||||
defcallback pop(t, key) :: {value, t}
|
||||
defcallback pop(t, key, value) :: {value, t}
|
||||
defcallback put(t, key, value) :: t
|
||||
defcallback put_new(t, key, value) :: t
|
||||
defcallback size(t) :: non_neg_integer()
|
||||
defcallback split(t, Enum.t) :: {t, t}
|
||||
defcallback take(t, Enum.t) :: t
|
||||
defcallback to_list(t) :: list()
|
||||
defcallback update(t, key, value, (value -> value)) :: t
|
||||
defcallback update!(t, key, (value -> value)) :: t | no_return
|
||||
defcallback values(t) :: list(value)
|
||||
|
||||
defmacrop target(dict) do
|
||||
quote do
|
||||
case unquote(dict) do
|
||||
%{__struct__: x} when is_atom(x) ->
|
||||
x
|
||||
%{} ->
|
||||
Map
|
||||
x when is_tuple(x) ->
|
||||
IO.write :stderr, "Using records with the Dict module is deprecated, please use structs instead\n#{Exception.format_stacktrace}"
|
||||
elem(x, 0)
|
||||
x when is_list(x) ->
|
||||
Keyword
|
||||
x ->
|
||||
unsupported_dict(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all keys in `dict`.
|
||||
The keys are not guaranteed to be in any order.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> Enum.sort(Dict.keys(d))
|
||||
[:a,:b]
|
||||
|
||||
"""
|
||||
@spec keys(t) :: [key]
|
||||
def keys(dict) do
|
||||
target(dict).keys(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all values in `dict`.
|
||||
The values are not guaranteed to be in any order.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> Enum.sort(Dict.values(d))
|
||||
[1,2]
|
||||
|
||||
"""
|
||||
@spec values(t) :: [value]
|
||||
def values(dict) do
|
||||
target(dict).values(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of elements in `dict`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> Dict.size(d)
|
||||
2
|
||||
|
||||
"""
|
||||
@spec size(t) :: non_neg_integer
|
||||
def size(dict) do
|
||||
target(dict).size(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the given `key` exists in the given `dict`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.has_key?(d, :a)
|
||||
true
|
||||
iex> Dict.has_key?(d, :b)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec has_key?(t, key) :: boolean
|
||||
def has_key?(dict, key) do
|
||||
target(dict).has_key?(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict`. If `dict` does not
|
||||
contain `key`, returns `default` (or `nil` if not provided).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.get(d, :a)
|
||||
1
|
||||
iex> Dict.get(d, :b)
|
||||
nil
|
||||
iex> Dict.get(d, :b, 3)
|
||||
3
|
||||
"""
|
||||
@spec get(t, key, value) :: value
|
||||
def get(dict, key, default \\ nil) do
|
||||
target(dict).get(dict, key, default)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `{:ok, value}` associated with `key` in `dict`.
|
||||
If `dict` does not contain `key`, returns `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.fetch(d, :a)
|
||||
{:ok, 1}
|
||||
iex> Dict.fetch(d, :b)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec fetch(t, key) :: value
|
||||
def fetch(dict, key) do
|
||||
target(dict).fetch(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict`. If `dict` does not
|
||||
contain `key`, it raises `KeyError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.fetch!(d, :a)
|
||||
1
|
||||
|
||||
"""
|
||||
@spec fetch!(t, key) :: value | no_return
|
||||
def fetch!(dict, key) do
|
||||
target(dict).fetch!(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the given `value` under `key` in `dict`.
|
||||
If `dict` already has `key`, the stored value is replaced by the new one.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.put(d, :a, 3)
|
||||
iex> Dict.get(d, :a)
|
||||
3
|
||||
|
||||
"""
|
||||
@spec put(t, key, value) :: t
|
||||
def put(dict, key, val) do
|
||||
target(dict).put(dict, key, val)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` in `dict` unless `key` already exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.put_new(d, :a, 3)
|
||||
iex> Dict.get(d, :a)
|
||||
1
|
||||
|
||||
"""
|
||||
@spec put_new(t, key, value) :: t
|
||||
def put_new(dict, key, val) do
|
||||
target(dict).put_new(dict, key, val)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the entry stored under the given `key` from `dict`.
|
||||
If `dict` does not contain `key`, returns the dictionary unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.delete(d, :a)
|
||||
iex> Dict.get(d, :a)
|
||||
nil
|
||||
|
||||
iex> d = Enum.into([b: 2], dict_impl.new)
|
||||
iex> Dict.delete(d, :a) == d
|
||||
true
|
||||
|
||||
"""
|
||||
@spec delete(t, key) :: t
|
||||
def delete(dict, key) do
|
||||
target(dict).delete(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges the dict `b` into dict `a`.
|
||||
|
||||
If one of the dict `b` entries already exists in the `dict`,
|
||||
the functions in entries in `b` have higher precedence unless a
|
||||
function is given to resolve conflicts.
|
||||
|
||||
Notice this function is polymorphic as it merges dicts of any
|
||||
type. Each dict implementation also provides a `merge` function,
|
||||
but they can only merge dicts of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d1 = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d2 = Enum.into([a: 3, d: 4], dict_impl.new)
|
||||
iex> d = Dict.merge(d1, d2)
|
||||
iex> [a: Dict.get(d, :a), b: Dict.get(d, :b), d: Dict.get(d, :d)]
|
||||
[a: 3, b: 2, d: 4]
|
||||
|
||||
iex> d1 = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d2 = Enum.into([a: 3, d: 4], dict_impl.new)
|
||||
iex> d = Dict.merge(d1, d2, fn(_k, v1, v2) ->
|
||||
...> v1 + v2
|
||||
...> end)
|
||||
iex> [a: Dict.get(d, :a), b: Dict.get(d, :b), d: Dict.get(d, :d)]
|
||||
[a: 4, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
@spec merge(t, t, (key, value, value -> value)) :: t
|
||||
def merge(dict1, dict2, fun \\ fn(_k, _v1, v2) -> v2 end) do
|
||||
target1 = target(dict1)
|
||||
target2 = target(dict2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.merge(dict1, dict2, fun)
|
||||
else
|
||||
Enumerable.reduce(dict2, {:cont, dict1}, fn({k, v}, acc) ->
|
||||
{:cont, target1.update(acc, k, v, fn(other) -> fun.(k, other, v) end)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict` as
|
||||
well as the `dict` without `key`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> {v, d} = Dict.pop dict, :a
|
||||
iex> {v, Enum.sort(d)}
|
||||
{1,[]}
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> {v, d} = Dict.pop dict, :b
|
||||
iex> {v, Enum.sort(d)}
|
||||
{nil,[a: 1]}
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> {v, d} = Dict.pop dict, :b, 3
|
||||
iex> {v, Enum.sort(d)}
|
||||
{3,[a: 1]}
|
||||
|
||||
"""
|
||||
@spec pop(t, key, value) :: {value, t}
|
||||
def pop(dict, key, default \\ nil) do
|
||||
target(dict).pop(dict, key, default)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a value in `dict` by calling `fun` on the value to get a new
|
||||
value. An exception is generated if `key` is not present in the dict.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.update!(d, :a, fn(val) -> -val end)
|
||||
iex> Dict.get(d, :a)
|
||||
-1
|
||||
|
||||
"""
|
||||
@spec update!(t, key, (value -> value)) :: t
|
||||
def update!(dict, key, fun) do
|
||||
target(dict).update!(dict, key, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a value in `dict` by calling `fun` on the value to get a new value. If
|
||||
`key` is not present in `dict` then `initial` will be stored as the first
|
||||
value.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.update(d, :c, 3, fn(val) -> -val end)
|
||||
iex> Dict.get(d, :c)
|
||||
3
|
||||
|
||||
"""
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(dict, key, initial, fun) do
|
||||
target(dict).update(dict, key, initial, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a tuple of two dicts, where the first dict contains only
|
||||
entries from `dict` with keys in `keys`, and the second dict
|
||||
contains only entries from `dict` with keys not in `keys`
|
||||
|
||||
Any non-member keys are ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2, c: 3, d: 4], dict_impl.new)
|
||||
iex> {d1, d2} = Dict.split(d, [:a, :c, :e])
|
||||
iex> {Dict.to_list(d1) |> Enum.sort, Dict.to_list(d2) |> Enum.sort}
|
||||
{[a: 1, c: 3], [b: 2, d: 4]}
|
||||
|
||||
iex> d = Enum.into([], dict_impl.new)
|
||||
iex> {d1, d2} = Dict.split(d, [:a, :c])
|
||||
iex> {Dict.to_list(d1), Dict.to_list(d2)}
|
||||
{[], []}
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> {d1, d2} = Dict.split(d, [:a, :b, :c])
|
||||
iex> {Dict.to_list(d1) |> Enum.sort, Dict.to_list(d2)}
|
||||
{[a: 1, b: 2], []}
|
||||
|
||||
"""
|
||||
@spec split(t, [key]) :: {t, t}
|
||||
def split(dict, keys) do
|
||||
target(dict).split(dict, keys)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a new dict where the given `keys` are removed from `dict`.
|
||||
Any non-member keys are ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.drop(d, [:a, :c, :d])
|
||||
iex> Dict.to_list(d)
|
||||
[b: 2]
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.drop(d, [:c, :d])
|
||||
iex> Dict.to_list(d) |> Enum.sort
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec drop(t, [key]) :: t
|
||||
def drop(dict, keys) do
|
||||
target(dict).drop(dict, keys)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a new dict where only the keys in `keys` from `dict` are included.
|
||||
|
||||
Any non-member keys are ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> d = Dict.take(d, [:a, :c, :d])
|
||||
iex> Dict.to_list(d)
|
||||
[a: 1]
|
||||
iex> d = Dict.take(d, [:c, :d])
|
||||
iex> Dict.to_list(d)
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec take(t, [key]) :: t
|
||||
def take(dict, keys) do
|
||||
target(dict).take(dict, keys)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec empty(t) :: t
|
||||
def empty(dict) do
|
||||
target(dict).empty(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if two dicts are equal using `===`.
|
||||
|
||||
Notice this function is polymorphic as it compares dicts of any
|
||||
type. Each dict implementation also provides an `equal?` function,
|
||||
but they can only compare dicts of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> a = Enum.into([a: 2, b: 3, f: 5, c: 123], dict_impl.new)
|
||||
iex> b = [a: 2, b: 3, f: 5, c: 123]
|
||||
iex> Dict.equal?(a, b)
|
||||
true
|
||||
|
||||
iex> a = Enum.into([a: 2, b: 3, f: 5, c: 123], dict_impl.new)
|
||||
iex> b = []
|
||||
iex> Dict.equal?(a, b)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(dict1, dict2) do
|
||||
target1 = target(dict1)
|
||||
target2 = target(dict2)
|
||||
|
||||
cond do
|
||||
target1 == target2 ->
|
||||
target1.equal?(dict1, dict2)
|
||||
|
||||
target1.size(dict1) == target2.size(dict2) ->
|
||||
Enumerable.reduce(dict2, {:cont, true}, fn({k, v}, _acc) ->
|
||||
case target1.fetch(dict1, k) do
|
||||
{:ok, ^v} -> {:cont, true}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of key-value pairs stored in `dict`.
|
||||
No particular order is enforced.
|
||||
"""
|
||||
@spec to_list(t) :: list
|
||||
def to_list(dict) do
|
||||
target(dict).to_list(dict)
|
||||
end
|
||||
|
||||
defp unsupported_dict(dict) do
|
||||
raise ArgumentError, message: "unsupported dict: #{inspect dict}"
|
||||
end
|
||||
end
|
||||
@@ -1,199 +0,0 @@
|
||||
defmodule Dict.Behaviour do
|
||||
@moduledoc ~S"""
|
||||
Default implementations for some required functions in the `Dict` module.
|
||||
|
||||
This module makes it easier to create your own `Dict` compliant
|
||||
module, by providing default implementations for some required functions.
|
||||
|
||||
## Example
|
||||
|
||||
defmodule MyDict do
|
||||
use Dict.Behaviour
|
||||
|
||||
# implement required functions (see below)
|
||||
|
||||
# override default implementations if optimization is needed
|
||||
end
|
||||
|
||||
The client module must contain the following functions:
|
||||
|
||||
* `delete/2`
|
||||
* `fetch/2`
|
||||
* `put/3`
|
||||
* `reduce/3`
|
||||
* `size/1`
|
||||
|
||||
All functions, except `reduce/3`, are required by the Dict behaviour.
|
||||
`reduce/3` must be implemtented as per the Enumerable protocol.
|
||||
|
||||
Based on these functions, `Dict.Behaviour` generates default implementations
|
||||
for the following functions:
|
||||
|
||||
* `drop/2`
|
||||
* `equal?/2`
|
||||
* `fetch!/2`
|
||||
* `get/2`
|
||||
* `get/3`
|
||||
* `has_key?/2`
|
||||
* `keys/1`
|
||||
* `merge/2`
|
||||
* `merge/3`
|
||||
* `pop/2`
|
||||
* `pop/3`
|
||||
* `put_new/3`
|
||||
* `split/2`
|
||||
* `take/2`
|
||||
* `to_list/1`
|
||||
* `values/1`
|
||||
* `update/4`
|
||||
* `update!/3`
|
||||
|
||||
All of these functions are defined as overridable, so you can provide your own
|
||||
implementation if needed.
|
||||
|
||||
Note you can also test your custom module via `Dict`'s doctests:
|
||||
|
||||
defmodule MyDict do
|
||||
def new(keywords \\ []) do
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MyTests do
|
||||
use ExUnit.Case
|
||||
doctest Dict
|
||||
defp dict_impl, do: MyDict
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro __using__(_) do
|
||||
# Use this import to guarantee proper code expansion
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
quote do
|
||||
@behaviour Dict
|
||||
|
||||
def get(dict, key, default \\ nil) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
def fetch!(dict, key) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise KeyError, key: key, term: dict
|
||||
end
|
||||
end
|
||||
|
||||
def has_key?(dict, key) do
|
||||
match? {:ok, _}, fetch(dict, key)
|
||||
end
|
||||
|
||||
def put_new(dict, key, value) do
|
||||
update(dict, key, value, fn(v) -> v end)
|
||||
end
|
||||
|
||||
def drop(dict, keys) do
|
||||
Enum.reduce keys, dict, &delete(&2, &1)
|
||||
end
|
||||
|
||||
def take(dict, keys) do
|
||||
Enum.reduce keys, new, fn key, acc ->
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> put(acc, key, value)
|
||||
:error -> acc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_list(dict) do
|
||||
reduce(dict, {:cont, []}, fn
|
||||
kv, acc -> {:cont, [kv|acc]}
|
||||
end) |> elem(1) |> :lists.reverse
|
||||
end
|
||||
|
||||
def keys(dict) do
|
||||
reduce(dict, {:cont, []}, fn
|
||||
{k, _}, acc -> {:cont, [k|acc]}
|
||||
end) |> elem(1) |> :lists.reverse
|
||||
end
|
||||
|
||||
def values(dict) do
|
||||
reduce(dict, {:cont, []}, fn
|
||||
{_, v}, acc -> {:cont, [v|acc]}
|
||||
end) |> elem(1) |> :lists.reverse
|
||||
end
|
||||
|
||||
def equal?(dict1, dict2) do
|
||||
# Use this import to avoid conflicts in the user code
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
case size(dict1) == size(dict2) do
|
||||
false -> false
|
||||
true ->
|
||||
reduce(dict1, {:cont, true}, fn({k, v}, _acc) ->
|
||||
case fetch(dict2, k) do
|
||||
{:ok, ^v} -> {:cont, true}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
def merge(dict1, dict2, fun \\ fn(_k, _v1, v2) -> v2 end) do
|
||||
reduce(dict1, {:cont, dict2}, fn {k, v1}, acc ->
|
||||
{:cont, update(acc, k, v1, &fun.(k, v1, &1))}
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
def update(dict, key, initial, fun) do
|
||||
case fetch(dict, key) do
|
||||
{ :ok, value } ->
|
||||
put(dict, key, fun.(value))
|
||||
|
||||
:error ->
|
||||
put(dict, key, initial)
|
||||
end
|
||||
end
|
||||
|
||||
def update!(dict, key, fun) do
|
||||
case fetch(dict, key) do
|
||||
{ :ok, value } ->
|
||||
put(dict, key, fun.(value))
|
||||
|
||||
:error ->
|
||||
raise KeyError, key: key, term: dict
|
||||
end
|
||||
end
|
||||
|
||||
def pop(dict, key, default \\ nil) do
|
||||
case fetch(dict, key) do
|
||||
{ :ok, value } ->
|
||||
{ value, delete(dict, key) }
|
||||
|
||||
:error ->
|
||||
{ default, dict }
|
||||
end
|
||||
end
|
||||
|
||||
def split(dict, keys) do
|
||||
Enum.reduce keys, { new, dict }, fn key, { inc, exc } = acc ->
|
||||
case fetch(exc, key) do
|
||||
{ :ok, value } ->
|
||||
{ put(inc, key, value), delete(exc, key) }
|
||||
|
||||
:error ->
|
||||
acc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defoverridable merge: 2, merge: 3, equal?: 2, to_list: 1, keys: 1,
|
||||
values: 1, take: 2, drop: 2, get: 2, get: 3, fetch!: 2,
|
||||
has_key?: 2, put_new: 3, pop: 2, pop: 3, update: 4, update!: 3,
|
||||
split: 2
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,677 +0,0 @@
|
||||
# Some exceptions implement `message/1` instead of `exception/1` mostly
|
||||
# for bootstrap reasons. It is recommended for applications to implement
|
||||
# `exception/1` instead of `message/1` as described in `defexception/3` docs.
|
||||
|
||||
defexception RuntimeError, message: "runtime error"
|
||||
defexception ArgumentError, message: "argument error"
|
||||
defexception ArithmeticError, message: "bad argument in arithmetic expression"
|
||||
defexception SystemLimitError, message: "a system limit has been reached"
|
||||
|
||||
defexception SyntaxError, [file: nil, line: nil, description: "syntax error"] do
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defexception TokenMissingError, [file: nil, line: nil, description: "expression is incomplete"] do
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defexception CompileError, [file: nil, line: nil, description: "compile error"] do
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defexception BadFunctionError, [term: nil] do
|
||||
def message(exception) do
|
||||
"expected a function, got: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception BadStructError, [struct: nil, term: nil] do
|
||||
def message(exception) do
|
||||
"expected a struct named #{inspect(exception.struct)}, got: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception MatchError, [term: nil] do
|
||||
def message(exception) do
|
||||
"no match of right hand side value: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception CaseClauseError, [term: nil] do
|
||||
def message(exception) do
|
||||
"no case clause matching: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception TryClauseError, [term: nil] do
|
||||
def message(exception) do
|
||||
"no try clause matching: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception BadArityError, [function: nil, args: nil] do
|
||||
def message(exception) do
|
||||
fun = exception.function
|
||||
args = exception.args
|
||||
insp = Enum.map_join(args, ", ", &inspect/1)
|
||||
{:arity, arity} = :erlang.fun_info(fun, :arity)
|
||||
"#{inspect(fun)} with arity #{arity} called with #{count(length(args), insp)}"
|
||||
end
|
||||
|
||||
defp count(0, _insp), do: "no arguments"
|
||||
defp count(1, insp), do: "1 argument (#{insp})"
|
||||
defp count(x, insp), do: "#{x} arguments (#{insp})"
|
||||
end
|
||||
|
||||
defexception UndefinedFunctionError, [module: nil, function: nil, arity: nil] do
|
||||
def message(exception) do
|
||||
if exception.function do
|
||||
formatted = Exception.format_mfa exception.module, exception.function, exception.arity
|
||||
"undefined function: #{formatted}"
|
||||
else
|
||||
"undefined function"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defexception FunctionClauseError, [module: nil, function: nil, arity: nil] do
|
||||
def message(exception) do
|
||||
if exception.function do
|
||||
formatted = Exception.format_mfa exception.module, exception.function, exception.arity
|
||||
"no function clause matching in #{formatted}"
|
||||
else
|
||||
"no function clause matches"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defexception Protocol.UndefinedError, [protocol: nil, value: nil, description: nil] do
|
||||
def message(exception) do
|
||||
msg = "protocol #{inspect exception.protocol} not implemented for #{inspect exception.value}"
|
||||
if exception.description do
|
||||
msg <> ", " <> exception.description
|
||||
else
|
||||
msg
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defexception ErlangError, [original: nil] do
|
||||
def message(exception) do
|
||||
"erlang error: #{inspect(exception.original)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception KeyError, key: nil, term: nil do
|
||||
def message(exception) do
|
||||
"key #{inspect exception.key} not found in: #{inspect exception.term}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception UnicodeConversionError, [:encoded, :message] do
|
||||
def exception(opts) do
|
||||
UnicodeConversionError[
|
||||
encoded: opts[:encoded],
|
||||
message: "#{opts[:kind]} #{detail(opts[:rest])}"
|
||||
]
|
||||
end
|
||||
|
||||
defp detail(rest) when is_binary(rest) do
|
||||
"encoding starting at #{inspect rest}"
|
||||
end
|
||||
|
||||
defp detail([h|_]) do
|
||||
"code point #{h}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception Enum.OutOfBoundsError, message: "out of bounds error"
|
||||
|
||||
defexception Enum.EmptyError, message: "empty error"
|
||||
|
||||
defexception File.Error, [reason: nil, action: "", path: nil] do
|
||||
def message(exception) do
|
||||
formatted = iodata_to_binary(:file.format_error(reason exception))
|
||||
"could not #{action exception} #{path exception}: #{formatted}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception File.CopyError, [reason: nil, action: "", source: nil, destination: nil, on: nil] do
|
||||
def message(exception) do
|
||||
formatted = iodata_to_binary(:file.format_error(reason exception))
|
||||
location = if on = on(exception), do: ". #{on}", else: ""
|
||||
"could not #{action exception} from #{source exception} to " <>
|
||||
"#{destination exception}#{location}: #{formatted}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Exception do
|
||||
@moduledoc """
|
||||
Functions to work with and pretty print exceptions.
|
||||
|
||||
Note that stacktraces in Elixir are updated on throw,
|
||||
errors and exits. For example, at any given moment,
|
||||
`System.stacktrace` will return the stacktrace for the
|
||||
last throw/error/exit that ocurred in the current process.
|
||||
|
||||
Finally note developers should not rely on the particular
|
||||
format of the `format` functions provided by this module.
|
||||
They may be changed in future releases in order to better
|
||||
suit Elixir's tool chain. In other words, by using the
|
||||
functions in this module it is guarantee you will format
|
||||
exceptions as in the current Elixir version being used.
|
||||
"""
|
||||
|
||||
@type t :: tuple
|
||||
@type stacktrace :: [stacktrace_entry]
|
||||
@type process ::
|
||||
pid | atom | {atom, node} | {:global, term} | {:via, module, term}
|
||||
@type stacktrace_entry ::
|
||||
{module, function, arity_or_args, location} |
|
||||
{function, arity_or_args, location}
|
||||
|
||||
@typep arity_or_args :: non_neg_integer | list
|
||||
@typep location :: Keyword.t
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument is an exception.
|
||||
"""
|
||||
def exception?(exception) do
|
||||
is_tuple(exception) and tuple_size(exception) > 1 and
|
||||
:erlang.element(2, exception) == :__exception__
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the message for an exception.
|
||||
"""
|
||||
def message(exception) do
|
||||
try do
|
||||
exception.message
|
||||
rescue
|
||||
e ->
|
||||
raise ArgumentError, message:
|
||||
"Got #{inspect e.__record__(:name)} with message " <>
|
||||
"\"#{message(e)}\" while retrieving message for #{inspect(exception)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalizes an exception, converting Erlang exceptions
|
||||
to Elixir exceptions.
|
||||
|
||||
It takes the `kind` spilled by `catch` as an argument and
|
||||
normalizes only `:error`, returning the untouched payload
|
||||
for others.
|
||||
|
||||
The third argument, a stacktrace, is optional. If it is
|
||||
not supplied `System.stacktrace/0` will sometimes be used
|
||||
to get additional information for the `kind` `:error`. If
|
||||
the stacktrace is unknown and `System.stacktrace/0` would
|
||||
not return the stacktrace corresponding to the exception
|
||||
an empty stacktrace, `[]`, must be used.
|
||||
"""
|
||||
@spec normalize(:error, any, stacktrace) :: t
|
||||
@spec normalize(:exit | :throw, any, stacktrace) :: any
|
||||
# Generating a stacktrace is expensive, default to nil to only fetch it when
|
||||
# needed.
|
||||
def normalize(kind, payload, stacktrace \\ nil)
|
||||
|
||||
def normalize(:error, exception, stacktrace) do
|
||||
if exception?(exception) do
|
||||
exception
|
||||
else
|
||||
normalize_error(exception, stacktrace)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(_kind, payload, _stacktrace) do
|
||||
payload
|
||||
end
|
||||
|
||||
defp normalize_error(:badarg, _stacktrace) do
|
||||
ArgumentError[]
|
||||
end
|
||||
|
||||
defp normalize_error(:badarith, _stacktrace) do
|
||||
ArithmeticError[]
|
||||
end
|
||||
|
||||
defp normalize_error(:system_limit, _stacktrace) do
|
||||
SystemLimitError[]
|
||||
end
|
||||
|
||||
defp normalize_error({:badarity, {fun, args}}, _stacktrace) do
|
||||
BadArityError[function: fun, args: args]
|
||||
end
|
||||
|
||||
defp normalize_error({:badfun, term}, _stacktrace) do
|
||||
BadFunctionError[term: term]
|
||||
end
|
||||
|
||||
defp normalize_error({:badstruct, struct, term}, _stacktrace) do
|
||||
BadStructError[struct: struct, term: term]
|
||||
end
|
||||
|
||||
defp normalize_error({:badmatch, term}, _stacktrace) do
|
||||
MatchError[term: term]
|
||||
end
|
||||
|
||||
defp normalize_error({:case_clause, term}, _stacktrace) do
|
||||
CaseClauseError[term: term]
|
||||
end
|
||||
|
||||
defp normalize_error({:try_clause, term}, _stacktrace) do
|
||||
TryClauseError[term: term]
|
||||
end
|
||||
|
||||
defp normalize_error(:undef, stacktrace) do
|
||||
{mod, fun, arity} = from_stacktrace(stacktrace || :erlang.get_stacktrace)
|
||||
UndefinedFunctionError[module: mod, function: fun, arity: arity]
|
||||
end
|
||||
|
||||
defp normalize_error(:function_clause, stacktrace) do
|
||||
{mod, fun, arity} = from_stacktrace(stacktrace || :erlang.get_stacktrace)
|
||||
FunctionClauseError[module: mod, function: fun, arity: arity]
|
||||
end
|
||||
|
||||
defp normalize_error({:badarg, payload}, _stacktrace) do
|
||||
ArgumentError[message: "argument error: #{inspect(payload)}"]
|
||||
end
|
||||
|
||||
defp normalize_error(other, _stacktrace) do
|
||||
ErlangError[original: other]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalizes and formats any throw, error and exit.
|
||||
|
||||
The message is formatted and displayed in the same
|
||||
format as used by Elixir's CLI.
|
||||
|
||||
The third argument, a stacktrace, is optional. If it is
|
||||
not supplied `System.stacktrace/0` will sometimes be used
|
||||
to get additional information for the `kind` `:error`. If
|
||||
the stacktrace is unknown and `System.stacktrace/0` would
|
||||
not return the stacktrace corresponding to the exception
|
||||
an empty stacktrace, `[]`, must be used.
|
||||
"""
|
||||
@spec format_banner(:error | :exit | :throw, any, stacktrace) :: String.t
|
||||
def format_banner(kind, exception, stacktrace \\ nil)
|
||||
|
||||
def format_banner(:error, exception, stacktrace) do
|
||||
exception = normalize(:error, exception, stacktrace)
|
||||
"** (" <> inspect(exception.__record__(:name)) <> ") " <> message(exception)
|
||||
end
|
||||
|
||||
def format_banner(:throw, reason, _stacktrace) do
|
||||
"** (throw) " <> inspect(reason)
|
||||
end
|
||||
|
||||
def format_banner(:exit, reason, _stacktrace) do
|
||||
"** (exit) " <> format_exit(reason, <<"\n ">>)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalizes and formats an exception and stacktrace.
|
||||
|
||||
Behaves the same as `format_banner/3` but also includes
|
||||
the formatted stacktrace.
|
||||
"""
|
||||
@spec format(:error | :exit | :throw, any, stacktrace) :: String.t
|
||||
def format(kind, exception, stacktrace \\ :erlang.get_stacktrace()) do
|
||||
message = format_banner(kind, exception, stacktrace)
|
||||
case stacktrace do
|
||||
[] -> message
|
||||
_ -> message <> "\n" <> format_stacktrace(stacktrace)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats an exit, returns a string.
|
||||
|
||||
Often there are errors/exceptions inside exits. Exits are often
|
||||
wrapped by the caller and provide stacktraces too. This function
|
||||
formats exits in a way to nicely show the exit reason, caller
|
||||
and stacktrace.
|
||||
"""
|
||||
@spec format_exit(any) :: String.t
|
||||
def format_exit(reason) do
|
||||
format_exit(reason, <<"\n ">>)
|
||||
end
|
||||
|
||||
# 2-Tuple could be caused by an error if the second element is a stacktrace.
|
||||
defp format_exit({exception, maybe_stacktrace} = reason, joiner)
|
||||
when is_list(maybe_stacktrace) and maybe_stacktrace !== [] do
|
||||
try do
|
||||
Enum.map(maybe_stacktrace, &format_stacktrace_entry/1)
|
||||
else
|
||||
formatted_stacktrace ->
|
||||
# Assume a non-empty list formattable as stacktrace is a
|
||||
# stacktrace, so exit was caused by an error.
|
||||
message = "an exception was raised:" <> joiner <>
|
||||
format_banner(:error, exception, maybe_stacktrace)
|
||||
Enum.join([message | formatted_stacktrace], joiner <> <<" ">>)
|
||||
catch
|
||||
:error, _ ->
|
||||
# Not a stacktrace, was an exit.
|
||||
format_exit_reason(reason)
|
||||
end
|
||||
end
|
||||
|
||||
# :supervisor.start_link returns this error reason when it fails to init
|
||||
# because a child's start_link raises.
|
||||
defp format_exit({:shutdown,
|
||||
{:failed_to_start_child, child, {:EXIT, reason}}}, joiner) do
|
||||
format_start_child(child, reason, joiner)
|
||||
end
|
||||
|
||||
# :supervisor.start_link returns this error reason when it fails to init
|
||||
# because a child's start_link returns {:error, reason}.
|
||||
defp format_exit({:shutdown, {:failed_to_start_child, child, reason}},
|
||||
joiner) do
|
||||
format_start_child(child, reason, joiner)
|
||||
end
|
||||
|
||||
# 2-Tuple could be an exit caused by mfa if second element is mfa, args
|
||||
# must be a list of arguments - max length 255 due to max arity.
|
||||
defp format_exit({reason2, {mod, fun, args}} = reason, joiner)
|
||||
when length(args) < 256 do
|
||||
try do
|
||||
format_mfa(mod, fun, args)
|
||||
else
|
||||
mfa ->
|
||||
# Assume tuple formattable as an mfa is an mfa, so exit was caused by
|
||||
# failed mfa.
|
||||
"exited in: " <> mfa <> joiner <>
|
||||
"** (EXIT) " <> format_exit(reason2, joiner <> <<" ">>)
|
||||
catch
|
||||
:error, _ ->
|
||||
# Not an mfa, was an exit.
|
||||
format_exit_reason(reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_exit(reason, _joiner) do
|
||||
format_exit_reason(reason)
|
||||
end
|
||||
|
||||
defp format_exit_reason(:normal), do: "normal"
|
||||
defp format_exit_reason(:shutdown), do: "shutdown"
|
||||
|
||||
defp format_exit_reason({:shutdown, reason}) do
|
||||
"shutdown: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
defp format_exit_reason(:timeout), do: "time out"
|
||||
defp format_exit_reason(:killed), do: "killed"
|
||||
defp format_exit_reason(:noconnection), do: "no connection"
|
||||
|
||||
defp format_exit_reason(:noproc) do
|
||||
"no process"
|
||||
end
|
||||
|
||||
defp format_exit_reason({:nodedown, node_name}) when is_atom(node_name) do
|
||||
"no connection to #{node_name}"
|
||||
end
|
||||
|
||||
# :gen_server exit reasons
|
||||
|
||||
defp format_exit_reason({:already_started, pid}) do
|
||||
"already started: " <> inspect(pid)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_return_value, value}) do
|
||||
"bad return value: " <> inspect(value)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_call, request}) do
|
||||
"bad call: " <> inspect(request)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_cast, request}) do
|
||||
"bad cast: " <> inspect(request)
|
||||
end
|
||||
|
||||
# :supervisor.start_link error reasons
|
||||
|
||||
# If value is a list will be be formatted by mfa exit in format_exit/1
|
||||
defp format_exit_reason({:bad_return, {mod, :init, value}})
|
||||
when is_atom(mod) do
|
||||
format_mfa(mod, :init, 1) <> " returned a bad value: " <> inspect(value)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_start_spec, start_spec}) do
|
||||
"bad start spec: invalid children: " <> inspect(start_spec)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:start_spec, start_spec}) do
|
||||
"bad start spec: " <> format_sup_spec(start_spec)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:supervisor_data, data}) do
|
||||
"bad supervisor data: " <> format_sup_data(data)
|
||||
end
|
||||
|
||||
defp format_exit_reason(reason), do: inspect(reason)
|
||||
|
||||
defp format_start_child(child, reason, joiner) do
|
||||
"shutdown: failed to start child: " <> inspect(child) <> joiner <>
|
||||
"** (EXIT) " <> format_exit(reason, joiner <> <<" ">>)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_type, type}) do
|
||||
"invalid type: " <> inspect(type)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_strategy, strategy}) do
|
||||
"invalid strategy: " <> inspect(strategy)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_intensity, intensity}) do
|
||||
"invalid intensity: " <> inspect(intensity)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_period, period}) do
|
||||
"invalid period: " <> inspect(period)
|
||||
end
|
||||
|
||||
defp format_sup_data(other), do: inspect(other)
|
||||
|
||||
defp format_sup_spec({:invalid_child_spec, child_spec}) do
|
||||
"invalid child spec: " <> inspect(child_spec)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_child_type, type}) do
|
||||
"invalid child type: " <> inspect(type)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_mfa, mfa}) do
|
||||
"invalid mfa: " <> inspect(mfa)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_restart_type, restart}) do
|
||||
"invalid restart type: " <> inspect(restart)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_shutdown, shutdown}) do
|
||||
"invalid shutdown: " <> inspect(shutdown)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_module, mod}) do
|
||||
"invalid module: " <> inspect(mod)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_modules, modules}) do
|
||||
"invalid modules: " <> inspect(modules)
|
||||
end
|
||||
|
||||
defp format_sup_spec(other), do: inspect(other)
|
||||
|
||||
@doc """
|
||||
Receives a stacktrace entry and formats it into a string.
|
||||
"""
|
||||
@spec format_stacktrace_entry(stacktrace_entry) :: String.t
|
||||
def format_stacktrace_entry(entry)
|
||||
|
||||
# From Macro.Env.stacktrace
|
||||
def format_stacktrace_entry({module, :__MODULE__, 0, location}) do
|
||||
format_location(location) <> inspect(module) <> " (module)"
|
||||
end
|
||||
|
||||
# From :elixir_compiler_*
|
||||
def format_stacktrace_entry({_module, :__MODULE__, 1, location}) do
|
||||
format_location(location) <> "(module)"
|
||||
end
|
||||
|
||||
# From :elixir_compiler_*
|
||||
def format_stacktrace_entry({_module, :__FILE__, 1, location}) do
|
||||
format_location(location) <> "(file)"
|
||||
end
|
||||
|
||||
def format_stacktrace_entry({module, fun, arity, location}) do
|
||||
format_application(module) <> format_location(location) <> format_mfa(module, fun, arity)
|
||||
end
|
||||
|
||||
def format_stacktrace_entry({fun, arity, location}) do
|
||||
format_location(location) <> format_fa(fun, arity)
|
||||
end
|
||||
|
||||
defp format_application(module) do
|
||||
case :application.get_application(module) do
|
||||
{:ok, app} -> "(" <> atom_to_binary(app) <> ") "
|
||||
:undefined -> ""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the stacktrace.
|
||||
|
||||
A stacktrace must be given as an argument. If not, the stacktrace
|
||||
is retrieved from `Process.info/2`.
|
||||
"""
|
||||
def format_stacktrace(trace \\ nil) do
|
||||
trace = trace || case Process.info(self, :current_stacktrace) do
|
||||
{:current_stacktrace, t} -> Enum.drop(t, 3)
|
||||
end
|
||||
|
||||
case trace do
|
||||
[] -> "\n"
|
||||
s -> " " <> Enum.map_join(s, "\n ", &format_stacktrace_entry(&1)) <> "\n"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives an anonymous function and arity and formats it as
|
||||
shown in stacktraces. The arity may also be a list of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
Exception.format_fa(fn -> end, 1)
|
||||
#=> "#Function<...>/1"
|
||||
|
||||
"""
|
||||
def format_fa(fun, arity) when is_function(fun) do
|
||||
"#{inspect fun}#{format_arity(arity)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a module, fun and arity and formats it
|
||||
as shown in stacktraces. The arity may also be a list
|
||||
of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Exception.format_mfa Foo, :bar, 1
|
||||
"Foo.bar/1"
|
||||
|
||||
iex> Exception.format_mfa Foo, :bar, []
|
||||
"Foo.bar()"
|
||||
|
||||
iex> Exception.format_mfa nil, :bar, []
|
||||
"nil.bar()"
|
||||
|
||||
Anonymous functions are reported as -func/arity-anonfn-count-,
|
||||
where func is the name of the enclosing function. Convert to
|
||||
"anonymous fn in func/arity"
|
||||
"""
|
||||
def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do
|
||||
fun =
|
||||
case inspect(fun) do
|
||||
":" <> fun -> fun
|
||||
fun -> fun
|
||||
end
|
||||
|
||||
case match?("\"-" <> _, fun) and String.split(fun, "-") do
|
||||
[ "\"", outer_fun, "fun", _count, "\"" ] ->
|
||||
"anonymous fn#{format_arity(arity)} in #{inspect module}.#{outer_fun}"
|
||||
_ ->
|
||||
"#{inspect module}.#{fun}#{format_arity(arity)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp format_arity(arity) when is_list(arity) do
|
||||
inspected = for x <- arity, do: inspect(x)
|
||||
"(#{Enum.join(inspected, ", ")})"
|
||||
end
|
||||
|
||||
defp format_arity(arity) when is_integer(arity) do
|
||||
"/" <> integer_to_binary(arity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the given file and line as shown in stacktraces.
|
||||
If any of the values are nil, they are omitted.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Exception.format_file_line("foo", 1)
|
||||
"foo:1:"
|
||||
|
||||
iex> Exception.format_file_line("foo", nil)
|
||||
"foo:"
|
||||
|
||||
iex> Exception.format_file_line(nil, nil)
|
||||
""
|
||||
|
||||
"""
|
||||
def format_file_line(file, line) do
|
||||
format_file_line(file, line, "")
|
||||
end
|
||||
|
||||
defp format_file_line(file, line, suffix) do
|
||||
if file do
|
||||
if line && line != 0 do
|
||||
"#{file}:#{line}:#{suffix}"
|
||||
else
|
||||
"#{file}:#{suffix}"
|
||||
end
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
defp format_location(opts) when is_list(opts) do
|
||||
format_file_line Keyword.get(opts, :file), Keyword.get(opts, :line), " "
|
||||
end
|
||||
|
||||
defp from_stacktrace([{module, function, args, _}|_]) when is_list(args) do
|
||||
{module, function, length(args)}
|
||||
end
|
||||
|
||||
defp from_stacktrace([{module, function, arity, _}|_]) do
|
||||
{module, function, arity}
|
||||
end
|
||||
|
||||
defp from_stacktrace(_) do
|
||||
{nil, nil, nil}
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,172 +0,0 @@
|
||||
defmodule Float do
|
||||
@moduledoc """
|
||||
Functions for working with floating point numbers.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Parses a binary into a float.
|
||||
|
||||
If successful, returns a tuple of the form `{float, remainder_of_binary}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.parse("34")
|
||||
{34.0,""}
|
||||
|
||||
iex> Float.parse("34.25")
|
||||
{34.25,""}
|
||||
|
||||
iex> Float.parse("56.5xyz")
|
||||
{56.5,"xyz"}
|
||||
|
||||
iex> Float.parse("pi")
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec parse(binary) :: {float, binary} | :error
|
||||
def parse("-" <> binary) do
|
||||
case parse_unsign(binary) do
|
||||
:error -> :error
|
||||
{number, remainder} -> {-number, remainder}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(binary) do
|
||||
parse_unsign(binary)
|
||||
end
|
||||
|
||||
defp parse_unsign("-" <> _), do: :error
|
||||
defp parse_unsign(binary) when is_binary(binary) do
|
||||
case Integer.parse binary do
|
||||
:error -> :error
|
||||
{integer_part, after_integer} -> parse_unsign after_integer, integer_part
|
||||
end
|
||||
end
|
||||
|
||||
# Dot followed by digit is required afterwards or we are done
|
||||
defp parse_unsign(<< ?., char, rest :: binary >>, int) when char in ?0..?9 do
|
||||
parse_unsign(rest, char - ?0, 1, int)
|
||||
end
|
||||
|
||||
defp parse_unsign(rest, int) do
|
||||
{:erlang.float(int), rest}
|
||||
end
|
||||
|
||||
# Handle decimal points
|
||||
defp parse_unsign(<< char, rest :: binary >>, float, decimal, int) when char in ?0..?9 do
|
||||
parse_unsign rest, 10 * float + (char - ?0), decimal + 1, int
|
||||
end
|
||||
|
||||
defp parse_unsign(<< ?e, after_e :: binary >>, float, decimal, int) do
|
||||
case Integer.parse after_e do
|
||||
:error ->
|
||||
# Note we rebuild the binary here instead of breaking it apart at
|
||||
# the function clause because the current approach copies a binary
|
||||
# just on this branch. If we broke it apart in the function clause,
|
||||
# the copy would happen when calling Integer.parse/1.
|
||||
{floatify(int, float, decimal), << ?e, after_e :: binary >>}
|
||||
{exponential, after_exponential} ->
|
||||
{floatify(int, float, decimal, exponential), after_exponential}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_unsign(bitstring, float, decimal, int) do
|
||||
{floatify(int, float, decimal), bitstring}
|
||||
end
|
||||
|
||||
defp floatify(int, float, decimal, exponential \\ 0) do
|
||||
multiplier = if int < 0, do: -1.0, else: 1.0
|
||||
|
||||
# Try to ensure the minimum amount of rounding errors
|
||||
result = multiplier * (abs(int) * :math.pow(10, decimal) + float) * :math.pow(10, exponential - decimal)
|
||||
|
||||
# Try avoiding stuff like this:
|
||||
# iex(1)> 0.0001 * 75
|
||||
# 0.007500000000000001
|
||||
# Due to IEEE 754 floating point standard
|
||||
# http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
||||
|
||||
final_decimal_places = decimal - exponential
|
||||
if final_decimal_places > 0 do
|
||||
decimal_power_round = :math.pow(10, final_decimal_places)
|
||||
trunc(result * decimal_power_round) / decimal_power_round
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest integer less than or equal to `num`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.floor(34)
|
||||
34
|
||||
|
||||
iex> Float.floor(34.25)
|
||||
34
|
||||
|
||||
iex> Float.floor(-56.5)
|
||||
-57
|
||||
|
||||
"""
|
||||
@spec floor(float | integer) :: integer
|
||||
def floor(num) when is_integer(num), do: num
|
||||
def floor(num) when is_float(num) do
|
||||
truncated = :erlang.trunc(num)
|
||||
case :erlang.abs(num - truncated) do
|
||||
x when x > 0 and num < 0 -> truncated - 1
|
||||
_ -> truncated
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest integer greater than or equal to `num`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.ceil(34)
|
||||
34
|
||||
|
||||
iex> Float.ceil(34.25)
|
||||
35
|
||||
|
||||
iex> Float.ceil(-56.5)
|
||||
-56
|
||||
|
||||
"""
|
||||
@spec ceil(float | integer) :: integer
|
||||
def ceil(num) when is_integer(num), do: num
|
||||
def ceil(num) when is_float(num) do
|
||||
truncated = :erlang.trunc(num)
|
||||
case :erlang.abs(num - truncated) do
|
||||
x when x > 0 and num > 0 -> truncated + 1
|
||||
_ -> truncated
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a floating point value to an arbitrary number of fractional digits
|
||||
(between 0 and 15).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.round(5.5674, 3)
|
||||
5.567
|
||||
|
||||
iex> Float.round(5.5675, 3)
|
||||
5.568
|
||||
|
||||
iex> Float.round(-5.5674, 3)
|
||||
-5.567
|
||||
|
||||
iex> Float.round(-5.5675, 3)
|
||||
-5.568
|
||||
|
||||
"""
|
||||
@spec round(float, integer) :: float
|
||||
def round(number, precision) when is_float(number) and is_integer(precision) and precision in 0..15 do
|
||||
Kernel.round(number * :math.pow(10, precision)) / :math.pow(10, precision)
|
||||
end
|
||||
end
|
||||
@@ -1,100 +0,0 @@
|
||||
defmodule GenEvent.Behaviour do
|
||||
@moduledoc """
|
||||
This module is a convenience for defining GenEvent callbacks in Elixir.
|
||||
|
||||
GenEvent is an OTP behaviour that encapsulates event handling functionality.
|
||||
|
||||
## Example
|
||||
|
||||
Below is an example of a GenEvent that stores notifications
|
||||
until they are fetched:
|
||||
|
||||
defmodule MyEventHandler do
|
||||
use GenEvent.Behaviour
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init(_) do
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
def handle_event({:notification, x}, notifications) do
|
||||
{:ok, [x|notifications]}
|
||||
end
|
||||
|
||||
def handle_call(:notifications, notifications) do
|
||||
{:ok, Enum.reverse(notifications), []}
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, pid} = :gen_event.start_link
|
||||
#=> {:ok,#PID<0.42.0>}
|
||||
|
||||
:gen_event.add_handler(pid, MyEventHandler, [])
|
||||
#=> :ok
|
||||
|
||||
:gen_event.notify(pid, {:notification, 1})
|
||||
#=> :ok
|
||||
|
||||
:gen_event.notify(pid, {:notification, 2})
|
||||
#=> :ok
|
||||
|
||||
:gen_event.call(pid, MyEventHandler, :notifications)
|
||||
#=> [1, 2]
|
||||
|
||||
:gen_event.call(pid, MyEventHandler, :notifications)
|
||||
#=> []
|
||||
|
||||
Notice we never call the server callbacks directly, they are called
|
||||
by OTP whenever we interact with the server.
|
||||
|
||||
Starting and sending messages to the GenEvent is done
|
||||
via Erlang's `:gen_event` module. For more information,
|
||||
please refer to the following:
|
||||
|
||||
* http://www.erlang.org/doc/man/gen_event.html
|
||||
* http://learnyousomeerlang.com/event-handlers
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :gen_event
|
||||
|
||||
@doc false
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_event(_event, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(_request, state) do
|
||||
{:ok, :ok, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def terminate(reason, state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defoverridable [init: 1,
|
||||
handle_event: 2,
|
||||
handle_call: 2, handle_info: 2,
|
||||
terminate: 2, code_change: 3]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,125 +0,0 @@
|
||||
defmodule GenServer.Behaviour do
|
||||
@moduledoc """
|
||||
This module is a convenience for defining GenServer callbacks in Elixir.
|
||||
|
||||
A server is responsible for reacting to messages received from a client.
|
||||
A GenServer is an OTP behaviour that encapsulates common server
|
||||
functionalities.
|
||||
|
||||
## Example
|
||||
|
||||
Below is an example of a GenServer that pushes and pops items
|
||||
onto a stack:
|
||||
|
||||
defmodule MyServer do
|
||||
use GenServer.Behaviour
|
||||
|
||||
# Callbacks
|
||||
|
||||
def handle_call(:pop, _from, [h|t]) do
|
||||
{:reply, h, t}
|
||||
end
|
||||
|
||||
def handle_call(request, from, config) do
|
||||
# Call the default implementation from GenServer.Behaviour
|
||||
super(request, from, config)
|
||||
end
|
||||
|
||||
def handle_cast({:push, item}, config) do
|
||||
{:noreply, [item|config]}
|
||||
end
|
||||
|
||||
def handle_cast(request, config) do
|
||||
super(request, config)
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, pid} = :gen_server.start_link(MyServer, [:hello], [])
|
||||
|
||||
:gen_server.call(pid, :pop)
|
||||
#=> :hello
|
||||
|
||||
:gen_server.cast(pid, {:push, :world})
|
||||
#=> :ok
|
||||
|
||||
:gen_server.call(pid, :pop)
|
||||
#=> :world
|
||||
|
||||
Notice we never call the server callbacks directly, they are called
|
||||
by OTP whenever we interact with the server. **cast** messages are
|
||||
asynchronous while **call** ones are synchronous. For a
|
||||
GenServer, there are 8 different values a callback such as
|
||||
`handle_call` or `handle_cast` can return:
|
||||
|
||||
{:reply, reply, new_state}
|
||||
{:reply, reply, new_state, timeout}
|
||||
{:reply, reply, new_state, :hibernate}
|
||||
{:noreply, new_state}
|
||||
{:noreply, new_state, timeout}
|
||||
{:noreply, new_state, :hibernate}
|
||||
{:stop, reason, new_state}
|
||||
{:stop, reason, reply, new_state}
|
||||
|
||||
There are 6 callbacks required to be implemented in a GenServer. The
|
||||
`GenServer.Behaviour` module defines all of them automatically, but
|
||||
allows us to customize the ones we need. The required callbacks are:
|
||||
|
||||
* `init(args)` - invoked when the server is started;
|
||||
* `handle_call(msg, from, state)` - invoked to handle call messages;
|
||||
* `handle_cast(msg, state)` - invoked to handle cast messages;
|
||||
* `handle_info(msg, state)` - handle all other messages which are
|
||||
normally received by processes;
|
||||
* `terminate(reason, state)` - called when the server is about to
|
||||
terminate, useful for cleaning up;
|
||||
* `code_change(old_vsn, state, extra)` - called when the application
|
||||
code is being upgraded live (hot code swap);
|
||||
|
||||
Starting and sending messages to the GenServer is done
|
||||
via Erlang's `:gen_server` module. For more information,
|
||||
please refer to the following:
|
||||
|
||||
* http://www.erlang.org/doc/man/gen_server.html
|
||||
* http://www.erlang.org/doc/design_principles/gen_server_concepts.html
|
||||
* http://learnyousomeerlang.com/clients-and-servers
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :gen_server
|
||||
|
||||
@doc false
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(request, _from, state) do
|
||||
{:stop, {:bad_call, request}, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_cast(msg, state) do
|
||||
{:stop, {:bad_cast, msg}, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defoverridable [init: 1, handle_call: 3, handle_info: 2,
|
||||
handle_cast: 2, terminate: 2, code_change: 3]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,271 +0,0 @@
|
||||
defmodule HashDict do
|
||||
@moduledoc """
|
||||
A key-value store.
|
||||
|
||||
The `HashDict` is represented internally as a struct, therefore
|
||||
`%HashDict{}` can be used whenever there is a need to match
|
||||
on any `HashDict`. Note though the struct fields are private and
|
||||
must not be accessed directly. Instead, use the functions on this
|
||||
or in the `Dict` module.
|
||||
|
||||
Implementation-wise, `HashDict` is implemented using tries, which
|
||||
grows in space as the number of keys grows, working well with both
|
||||
small and large set of keys. For more information about the
|
||||
functions and their APIs, please consult the `Dict` module.
|
||||
"""
|
||||
|
||||
use Dict.Behaviour
|
||||
|
||||
@node_bitmap 0b111
|
||||
@node_shift 3
|
||||
@node_size 8
|
||||
@node_template :erlang.make_tuple(@node_size, [])
|
||||
|
||||
@opaque t :: map
|
||||
defstruct size: 0, root: @node_template
|
||||
|
||||
# Inline common instructions
|
||||
@compile :inline_list_funcs
|
||||
@compile {:inline, key_hash: 1, key_mask: 1, key_shift: 1}
|
||||
|
||||
@doc """
|
||||
Creates a new empty dict.
|
||||
"""
|
||||
@spec new :: Dict.t
|
||||
def new do
|
||||
%HashDict{}
|
||||
end
|
||||
|
||||
def put(%HashDict{root: root, size: size}, key, value) do
|
||||
{root, counter} = do_put(root, key, value, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def update!(%HashDict{root: root, size: size} = dict, key, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> raise KeyError, key: key, term: dict end,
|
||||
fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def update(%HashDict{root: root, size: size}, key, initial, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> initial end, fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def fetch(%HashDict{root: root}, key) do
|
||||
do_fetch(root, key, key_hash(key))
|
||||
end
|
||||
|
||||
def delete(dict, key) do
|
||||
case dict_delete(dict, key) do
|
||||
{dict, _value} -> dict
|
||||
:error -> dict
|
||||
end
|
||||
end
|
||||
|
||||
def pop(dict, key, default \\ nil) do
|
||||
case dict_delete(dict, key) do
|
||||
{dict, value} -> {value, dict}
|
||||
:error -> {default, dict}
|
||||
end
|
||||
end
|
||||
|
||||
def size(%HashDict{size: size}) do
|
||||
size
|
||||
end
|
||||
|
||||
@doc false
|
||||
def reduce(%HashDict{root: root}, acc, fun) do
|
||||
do_reduce(root, acc, fun, @node_size, fn
|
||||
{:suspend, acc} -> {:suspended, acc, &{:done, elem(&1, 1)}}
|
||||
{:halt, acc} -> {:halted, acc}
|
||||
{:cont, acc} -> {:done, acc}
|
||||
end)
|
||||
end
|
||||
|
||||
def split(dict, keys) do
|
||||
Enum.reduce keys, {new, dict}, fn key, {inc, exc} = acc ->
|
||||
case dict_delete(exc, key) do
|
||||
{exc, value} -> {put(inc, key, value), exc}
|
||||
:error -> acc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def merge(%HashDict{size: size1} = dict1, %HashDict{size: size2} = dict2, callback) when size1 < size2 do
|
||||
reduce(dict1, {:cont, dict2}, fn {k, v1}, acc ->
|
||||
{:cont, update(acc, k, v1, &callback.(k, v1, &1))}
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
def merge(%HashDict{} = dict1, %HashDict{} = dict2, callback) do
|
||||
reduce(dict2, {:cont, dict1}, fn {k, v2}, acc ->
|
||||
{:cont, update(acc, k, v2, &callback.(k, &1, v2))}
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
## General helpers
|
||||
|
||||
defp dict_delete(%HashDict{root: root, size: size}, key) do
|
||||
case do_delete(root, key, key_hash(key)) do
|
||||
{root, value} -> {%HashDict{root: root, size: size - 1}, value}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
## Dict manipulation
|
||||
|
||||
defp do_fetch(node, key, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[^key|v] -> {:ok, v}
|
||||
{^key, v, _} -> {:ok, v}
|
||||
{_, _, n} -> do_fetch(n, key, key_shift(hash))
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp do_put(node, key, value, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{set_elem(node, index, [key|value]), 1}
|
||||
[^key|_] ->
|
||||
{set_elem(node, index, [key|value]), 0}
|
||||
[k|v] ->
|
||||
n = set_elem(@node_template, key_mask(key_shift(hash)), [key|value])
|
||||
{set_elem(node, index, {k, v, n}), 1}
|
||||
{^key, _, n} ->
|
||||
{set_elem(node, index, {key, value, n}), 0}
|
||||
{k, v, n} ->
|
||||
{n, counter} = do_put(n, key, value, key_shift(hash))
|
||||
{set_elem(node, index, {k, v, n}), counter}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(node, key, initial, fun, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{set_elem(node, index, [key|initial.()]), 1}
|
||||
[^key|value] ->
|
||||
{set_elem(node, index, [key|fun.(value)]), 0}
|
||||
[k|v] ->
|
||||
n = set_elem(@node_template, key_mask(key_shift(hash)), [key|initial.()])
|
||||
{set_elem(node, index, {k, v, n}), 1}
|
||||
{^key, value, n} ->
|
||||
{set_elem(node, index, {key, fun.(value), n}), 0}
|
||||
{k, v, n} ->
|
||||
{n, counter} = do_update(n, key, initial, fun, key_shift(hash))
|
||||
{set_elem(node, index, {k, v, n}), counter}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete(node, key, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
:error
|
||||
[^key|value] ->
|
||||
{set_elem(node, index, []), value}
|
||||
[_|_] ->
|
||||
:error
|
||||
{^key, value, n} ->
|
||||
{set_elem(node, index, do_compact_node(n)), value}
|
||||
{k, v, n} ->
|
||||
case do_delete(n, key, key_shift(hash)) do
|
||||
{@node_template, value} ->
|
||||
{set_elem(node, index, [k|v]), value}
|
||||
{n, value} ->
|
||||
{set_elem(node, index, {k, v, n}), value}
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Enum.each 0..(@node_size - 1), fn index ->
|
||||
defp do_compact_node(node) when elem(node, unquote(index)) != [] do
|
||||
case elem(node, unquote(index)) do
|
||||
[k|v] ->
|
||||
case set_elem(node, unquote(index), []) do
|
||||
@node_template -> [k|v]
|
||||
n -> {k, v, n}
|
||||
end
|
||||
{k, v, n} ->
|
||||
{k, v, set_elem(node, unquote(index), do_compact_node(n))}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Dict reduce
|
||||
|
||||
defp do_reduce_each(_node, {:halt, acc}, _fun, _next) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_reduce_each(node, {:suspend, acc}, fun, next) do
|
||||
{:suspended, acc, &do_reduce_each(node, &1, fun, next)}
|
||||
end
|
||||
|
||||
defp do_reduce_each([], acc, _fun, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
defp do_reduce_each([k|v], {:cont, acc}, fun, next) do
|
||||
next.(fun.({k,v}, acc))
|
||||
end
|
||||
|
||||
defp do_reduce_each({k, v, n}, {:cont, acc}, fun, next) do
|
||||
do_reduce(n, fun.({k, v}, acc), fun, @node_size, next)
|
||||
end
|
||||
|
||||
defp do_reduce(node, acc, fun, count, next) when count > 0 do
|
||||
do_reduce_each(:erlang.element(count, node), acc, fun, &do_reduce(node, &1, fun, count - 1, next))
|
||||
end
|
||||
|
||||
defp do_reduce(_node, acc, _fun, 0, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
## Key operations
|
||||
|
||||
import Bitwise
|
||||
|
||||
defp key_hash(key) do
|
||||
:erlang.phash2(key)
|
||||
end
|
||||
|
||||
defp key_mask(hash) do
|
||||
hash &&& @node_bitmap
|
||||
end
|
||||
|
||||
defp key_shift(hash) do
|
||||
hash >>> @node_shift
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: HashDict do
|
||||
def reduce(dict, acc, fun), do: HashDict.reduce(dict, acc, fun)
|
||||
def member?(dict, {k, v}), do: {:ok, match?({:ok, ^v}, HashDict.fetch(dict, k))}
|
||||
def member?(_dict, _), do: {:ok, false}
|
||||
def count(dict), do: {:ok, HashDict.size(dict)}
|
||||
end
|
||||
|
||||
defimpl Access, for: HashDict do
|
||||
def access(dict, key), do: HashDict.get(dict, key, nil)
|
||||
end
|
||||
|
||||
defimpl Collectable, for: HashDict do
|
||||
def empty(_dict) do
|
||||
HashDict.new
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{original, fn
|
||||
dict, {:cont, {k, v}} -> Dict.put(dict, k, v)
|
||||
dict, :done -> dict
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
@@ -1,266 +0,0 @@
|
||||
defmodule HashSet do
|
||||
@moduledoc """
|
||||
A set store.
|
||||
|
||||
The `HashSet` is represented internally as a struct, therefore
|
||||
`%HashSet{}` can be used whenever there is a need to match
|
||||
on any `HashSet`. Note though the struct fields are private and
|
||||
must not be accessed directly. Instead, use the functions on this
|
||||
or in the `Set` module.
|
||||
|
||||
The `HashSet` is implemented using tries, which grows in
|
||||
space as the number of keys grows, working well with both
|
||||
small and large set of keys. For more information about the
|
||||
functions and their APIs, please consult the `Set` module.
|
||||
"""
|
||||
|
||||
@behaviour Set
|
||||
|
||||
@node_bitmap 0b111
|
||||
@node_shift 3
|
||||
@node_size 8
|
||||
@node_template :erlang.make_tuple(@node_size, [])
|
||||
|
||||
@opaque t :: map
|
||||
defstruct size: 0, root: @node_template
|
||||
|
||||
# Inline common instructions
|
||||
@compile :inline_list_funcs
|
||||
@compile {:inline, key_hash: 1, key_mask: 1, key_shift: 1}
|
||||
|
||||
@doc """
|
||||
Creates a new empty set.
|
||||
"""
|
||||
@spec new :: Set.t
|
||||
def new do
|
||||
%HashSet{}
|
||||
end
|
||||
|
||||
def union(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) when size1 <= size2 do
|
||||
set_fold set1, set2, fn v, acc -> put(acc, v) end
|
||||
end
|
||||
|
||||
def union(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold set2, set1, fn v, acc -> put(acc, v) end
|
||||
end
|
||||
|
||||
def intersection(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold set1, %HashSet{}, fn v, acc ->
|
||||
if member?(set2, v), do: put(acc, v), else: acc
|
||||
end
|
||||
end
|
||||
|
||||
def difference(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold set2, set1, fn v, acc -> delete(acc, v) end
|
||||
end
|
||||
|
||||
def to_list(set) do
|
||||
set_fold(set, [], &[&1|&2]) |> :lists.reverse
|
||||
end
|
||||
|
||||
def equal?(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) do
|
||||
case size1 do
|
||||
^size2 -> subset?(set1, set2)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def subset?(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
reduce(set1, {:cont, true}, fn member, acc ->
|
||||
case member?(set2, member) do
|
||||
true -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
def disjoint?(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
reduce(set2, {:cont, true}, fn member, acc ->
|
||||
case member?(set1, member) do
|
||||
false -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
def member?(%HashSet{root: root}, term) do
|
||||
do_member?(root, term, key_hash(term))
|
||||
end
|
||||
|
||||
def put(%HashSet{root: root, size: size}, term) do
|
||||
{root, counter} = do_put(root, term, key_hash(term))
|
||||
%HashSet{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def delete(%HashSet{root: root, size: size} = set, term) do
|
||||
case do_delete(root, term, key_hash(term)) do
|
||||
{:ok, root} -> %HashSet{root: root, size: size - 1}
|
||||
:error -> set
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def reduce(%HashSet{root: root}, acc, fun) do
|
||||
do_reduce(root, acc, fun, @node_size, fn
|
||||
{:suspend, acc} -> {:suspended, acc, &{:done, elem(&1, 1)}}
|
||||
{:halt, acc} -> {:halted, acc}
|
||||
{:cont, acc} -> {:done, acc}
|
||||
end)
|
||||
end
|
||||
|
||||
def size(%HashSet{size: size}) do
|
||||
size
|
||||
end
|
||||
|
||||
## Set helpers
|
||||
|
||||
defp set_fold(%HashSet{root: root}, acc, fun) do
|
||||
do_fold(root, acc, fun, @node_size)
|
||||
end
|
||||
|
||||
## Set manipulation
|
||||
|
||||
defp do_member?(node, term, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] -> false
|
||||
[^term|_] -> true
|
||||
[_] -> false
|
||||
[_|n] -> do_member?(n, term, key_shift(hash))
|
||||
end
|
||||
end
|
||||
|
||||
defp do_put(node, term, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{set_elem(node, index, [term]), 1}
|
||||
[^term|_] ->
|
||||
{node, 0}
|
||||
[t] ->
|
||||
n = set_elem(@node_template, key_mask(key_shift(hash)), [term])
|
||||
{set_elem(node, index, [t|n]), 1}
|
||||
[t|n] ->
|
||||
{n, counter} = do_put(n, term, key_shift(hash))
|
||||
{set_elem(node, index, [t|n]), counter}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete(node, term, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
:error
|
||||
[^term] ->
|
||||
{:ok, set_elem(node, index, [])}
|
||||
[_] ->
|
||||
:error
|
||||
[^term|n] ->
|
||||
{:ok, set_elem(node, index, do_compact_node(n))}
|
||||
[t|n] ->
|
||||
case do_delete(n, term, key_shift(hash)) do
|
||||
{:ok, @node_template} ->
|
||||
{:ok, set_elem(node, index, [t])}
|
||||
{:ok, n} ->
|
||||
{:ok, set_elem(node, index, [t|n])}
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Enum.each 0..(@node_size - 1), fn index ->
|
||||
defp do_compact_node(node) when elem(node, unquote(index)) != [] do
|
||||
case elem(node, unquote(index)) do
|
||||
[t] ->
|
||||
case set_elem(node, unquote(index), []) do
|
||||
@node_template -> [t]
|
||||
n -> [t|n]
|
||||
end
|
||||
[t|n] ->
|
||||
[t|set_elem(node, unquote(index), do_compact_node(n))]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Set fold
|
||||
|
||||
defp do_fold_each([], acc, _fun), do: acc
|
||||
defp do_fold_each([t], acc, fun), do: fun.(t, acc)
|
||||
defp do_fold_each([t|n], acc, fun), do: do_fold(n, fun.(t, acc), fun, @node_size)
|
||||
|
||||
defp do_fold(node, acc, fun, count) when count > 0 do
|
||||
acc = do_fold_each(:erlang.element(count, node), acc, fun)
|
||||
do_fold(node, acc, fun, count - 1)
|
||||
end
|
||||
|
||||
defp do_fold(_node, acc, _fun, 0) do
|
||||
acc
|
||||
end
|
||||
|
||||
## Set reduce
|
||||
|
||||
defp do_reduce_each(_node, {:halt, acc}, _fun, _next) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_reduce_each(node, {:suspend, acc}, fun, next) do
|
||||
{:suspended, acc, &do_reduce_each(node, &1, fun, next)}
|
||||
end
|
||||
|
||||
defp do_reduce_each([], acc, _fun, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
defp do_reduce_each([t], {:cont, acc}, fun, next) do
|
||||
next.(fun.(t, acc))
|
||||
end
|
||||
|
||||
defp do_reduce_each([t|n], {:cont, acc}, fun, next) do
|
||||
do_reduce(n, fun.(t, acc), fun, @node_size, next)
|
||||
end
|
||||
|
||||
defp do_reduce(node, acc, fun, count, next) when count > 0 do
|
||||
do_reduce_each(:erlang.element(count, node), acc, fun, &do_reduce(node, &1, fun, count - 1, next))
|
||||
end
|
||||
|
||||
defp do_reduce(_node, acc, _fun, 0, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
## Key operations
|
||||
|
||||
import Bitwise
|
||||
|
||||
defp key_hash(key) do
|
||||
:erlang.phash2(key)
|
||||
end
|
||||
|
||||
defp key_mask(hash) do
|
||||
hash &&& @node_bitmap
|
||||
end
|
||||
|
||||
defp key_shift(hash) do
|
||||
hash >>> @node_shift
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: HashSet do
|
||||
def reduce(set, acc, fun), do: HashSet.reduce(set, acc, fun)
|
||||
def member?(set, v), do: {:ok, HashSet.member?(set, v)}
|
||||
def count(set), do: {:ok, HashSet.size(set)}
|
||||
end
|
||||
|
||||
defimpl Collectable, for: HashSet do
|
||||
def empty(_dict) do
|
||||
HashSet.new
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{original, fn
|
||||
set, {:cont, x} -> HashSet.put(set, x)
|
||||
set, :done -> set
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
@@ -1,518 +0,0 @@
|
||||
import Kernel, except: [inspect: 1]
|
||||
import Inspect.Algebra
|
||||
|
||||
defprotocol Inspect do
|
||||
@moduledoc """
|
||||
The `Inspect` protocol is responsible for converting any Elixir
|
||||
data structure into an algebra document. This document is then
|
||||
formatted, either in pretty printing format or a regular one.
|
||||
|
||||
The `inspect/2` function receives the entity to be inspected
|
||||
followed by the inspecting options, represented by the record
|
||||
`Inspect.Opts`.
|
||||
|
||||
Inspection is done using the functions available in `Inspect.Algebra`.
|
||||
|
||||
## Examples
|
||||
|
||||
Many times, inspecting a structure can be implemented in function
|
||||
of existing entities. For example, here is `HashSet`'s `inspect`
|
||||
implementation:
|
||||
|
||||
defimpl Inspect, for: HashSet do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
concat ["#HashSet<", to_doc(HashSet.to_list(dict), opts), ">"]
|
||||
end
|
||||
end
|
||||
|
||||
The `concat` function comes from `Inspect.Algebra` and it
|
||||
concatenates algebra documents together. In the example above,
|
||||
it is concatenating the string `"HashSet<"` (all strings are
|
||||
valid algebra documents that keep their formatting when pretty
|
||||
printed), the document returned by `Inspect.Algebra.to_doc/2` and the
|
||||
other string `">"`.
|
||||
|
||||
Since regular strings are valid entities in an algebra document,
|
||||
an implementation of inspect may simply return a string,
|
||||
although that will devoid it of any pretty-printing.
|
||||
|
||||
## Error handling
|
||||
|
||||
In case there is an error while your structure is being inspected,
|
||||
Elixir will automatically fall back to a raw representation.
|
||||
|
||||
You can however access the underlying error by invoking the Inspect
|
||||
implementation directly. For example, to test Inspect.HashSet above,
|
||||
you can invoke it as:
|
||||
|
||||
Inspect.HashSet.inspect(HashSet.new, Inspect.Opts.new)
|
||||
|
||||
"""
|
||||
|
||||
# Handle structs in Any
|
||||
@fallback_to_any true
|
||||
|
||||
def inspect(thing, opts)
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, _opts) do
|
||||
inspect(atom)
|
||||
end
|
||||
|
||||
def inspect(false), do: "false"
|
||||
def inspect(true), do: "true"
|
||||
def inspect(nil), do: "nil"
|
||||
def inspect(:""), do: ":\"\""
|
||||
|
||||
def inspect(atom) do
|
||||
binary = atom_to_binary(atom)
|
||||
|
||||
cond do
|
||||
valid_ref_identifier?(binary) ->
|
||||
if only_elixir?(binary) do
|
||||
binary
|
||||
else
|
||||
"Elixir." <> rest = binary
|
||||
rest
|
||||
end
|
||||
valid_atom_identifier?(binary) ->
|
||||
":" <> binary
|
||||
atom in [:%{}, :{}, :<<>>, :..., :%] ->
|
||||
":" <> binary
|
||||
atom in Macro.binary_ops or atom in Macro.unary_ops ->
|
||||
":" <> binary
|
||||
true ->
|
||||
<< ?:, ?", Inspect.BitString.escape(binary, ?") :: binary, ?" >>
|
||||
end
|
||||
end
|
||||
|
||||
defp only_elixir?("Elixir." <> rest), do: only_elixir?(rest)
|
||||
defp only_elixir?("Elixir"), do: true
|
||||
defp only_elixir?(_), do: false
|
||||
|
||||
# Detect if atom is an atom alias (Elixir.Foo.Bar.Baz)
|
||||
|
||||
defp valid_ref_identifier?("Elixir" <> rest) do
|
||||
valid_ref_piece?(rest)
|
||||
end
|
||||
|
||||
defp valid_ref_identifier?(_), do: false
|
||||
|
||||
defp valid_ref_piece?(<<?., h, t :: binary>>) when h in ?A..?Z do
|
||||
valid_ref_piece? valid_identifier?(t)
|
||||
end
|
||||
|
||||
defp valid_ref_piece?(<<>>), do: true
|
||||
defp valid_ref_piece?(_), do: false
|
||||
|
||||
# Detect if atom
|
||||
|
||||
defp valid_atom_identifier?(<<h, t :: binary>>) when h in ?a..?z or h in ?A..?Z or h == ?_ do
|
||||
valid_atom_piece?(t)
|
||||
end
|
||||
|
||||
defp valid_atom_identifier?(_), do: false
|
||||
|
||||
defp valid_atom_piece?(t) do
|
||||
case valid_identifier?(t) do
|
||||
<<>> -> true
|
||||
<<??>> -> true
|
||||
<<?!>> -> true
|
||||
<<?@, t::binary>> -> valid_atom_piece?(t)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_identifier?(<<h, t :: binary>>)
|
||||
when h in ?a..?z
|
||||
when h in ?A..?Z
|
||||
when h in ?0..?9
|
||||
when h == ?_ do
|
||||
valid_identifier? t
|
||||
end
|
||||
|
||||
defp valid_identifier?(other), do: other
|
||||
end
|
||||
|
||||
defimpl Inspect, for: BitString do
|
||||
def inspect(thing, %Inspect.Opts{binaries: bins} = opts) when is_binary(thing) do
|
||||
if bins == :as_strings or (bins == :infer and String.printable?(thing)) do
|
||||
<< ?", escape(thing, ?") :: binary, ?" >>
|
||||
else
|
||||
inspect_bitstring(thing, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(thing, opts) do
|
||||
inspect_bitstring(thing, opts)
|
||||
end
|
||||
|
||||
## Escaping
|
||||
|
||||
@doc false
|
||||
def escape(other, char) do
|
||||
escape(other, char, <<>>)
|
||||
end
|
||||
|
||||
defp escape(<< char, t :: binary >>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, char >>)
|
||||
end
|
||||
defp escape(<<?#, ?{, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?#, ?{>>)
|
||||
end
|
||||
defp escape(<<?\a, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?a >>)
|
||||
end
|
||||
defp escape(<<?\b, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?b >>)
|
||||
end
|
||||
defp escape(<<?\d, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?d >>)
|
||||
end
|
||||
defp escape(<<?\e, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?e >>)
|
||||
end
|
||||
defp escape(<<?\f, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?f >>)
|
||||
end
|
||||
defp escape(<<?\n, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?n >>)
|
||||
end
|
||||
defp escape(<<?\r, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?r >>)
|
||||
end
|
||||
defp escape(<<?\\, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?\\ >>)
|
||||
end
|
||||
defp escape(<<?\t, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?t >>)
|
||||
end
|
||||
defp escape(<<?\v, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?v >>)
|
||||
end
|
||||
defp escape(<<h :: utf8, t :: binary>>, char, binary) do
|
||||
head = << h :: utf8 >>
|
||||
if String.printable?(head) do
|
||||
escape(t, char, append(head, binary))
|
||||
else
|
||||
<< byte :: size(8), h :: binary >> = head
|
||||
t = << h :: binary, t :: binary >>
|
||||
escape(t, char, << binary :: binary, octify(byte) :: binary >>)
|
||||
end
|
||||
end
|
||||
defp escape(<<h, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, octify(h) :: binary >>)
|
||||
end
|
||||
defp escape(<<>>, _char, binary), do: binary
|
||||
|
||||
defp octify(byte) do
|
||||
<< hi :: size(2), mi :: size(3), lo :: size(3) >> = << byte >>
|
||||
<< ?\\, ?0 + hi, ?0 + mi, ?0 + lo >>
|
||||
end
|
||||
|
||||
defp append(<<h, t :: binary>>, binary), do: append(t, << binary :: binary, h >>)
|
||||
defp append(<<>>, binary), do: binary
|
||||
|
||||
## Bitstrings
|
||||
|
||||
defp inspect_bitstring(bitstring, opts) do
|
||||
each_bit(bitstring, opts.limit, "<<") <> ">>"
|
||||
end
|
||||
|
||||
defp each_bit(_, 0, acc) do
|
||||
acc <> "..."
|
||||
end
|
||||
|
||||
defp each_bit(<<h, t :: bitstring>>, counter, acc) when t != <<>> do
|
||||
each_bit(t, decrement(counter), acc <> integer_to_binary(h) <> ", ")
|
||||
end
|
||||
|
||||
defp each_bit(<<h :: size(8)>>, _counter, acc) do
|
||||
acc <> integer_to_binary(h)
|
||||
end
|
||||
|
||||
defp each_bit(<<>>, _counter, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp each_bit(bitstring, _counter, acc) do
|
||||
size = bit_size(bitstring)
|
||||
<<h :: size(size)>> = bitstring
|
||||
acc <> integer_to_binary(h) <> "::size(" <> integer_to_binary(size) <> ")"
|
||||
end
|
||||
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
end
|
||||
|
||||
defimpl Inspect, for: List do
|
||||
@doc ~S"""
|
||||
Represents a list, checking if it can be printed or not.
|
||||
If so, a single-quoted representation is returned,
|
||||
otherwise the brackets syntax is used. Keywords are
|
||||
printed in keywords syntax.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> inspect('bar')
|
||||
"'bar'"
|
||||
|
||||
iex> inspect([0|'bar'])
|
||||
"[0, 98, 97, 114]"
|
||||
|
||||
iex> inspect([:foo,:bar])
|
||||
"[:foo, :bar]"
|
||||
|
||||
"""
|
||||
|
||||
def inspect([], _opts), do: "[]"
|
||||
|
||||
def inspect(thing, %Inspect.Opts{char_lists: lists} = opts) do
|
||||
cond do
|
||||
lists == :as_char_lists or (lists == :infer and printable?(thing)) ->
|
||||
<< ?', Inspect.BitString.escape(String.from_char_data!(thing), ?') :: binary, ?' >>
|
||||
keyword?(thing) ->
|
||||
surround_many("[", thing, "]", opts.limit, &keyword(&1, opts))
|
||||
true ->
|
||||
surround_many("[", thing, "]", opts.limit, &to_doc(&1, opts))
|
||||
end
|
||||
end
|
||||
|
||||
def keyword({key, value}, opts) do
|
||||
concat(
|
||||
key_to_binary(key) <> ": ",
|
||||
to_doc(value, opts)
|
||||
)
|
||||
end
|
||||
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case atom_to_list(key) do
|
||||
'Elixir.' ++ _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
|
||||
def keyword?([]), do: true
|
||||
def keyword?(_other), do: false
|
||||
|
||||
## Private
|
||||
|
||||
defp key_to_binary(key) do
|
||||
case Inspect.Atom.inspect(key) do
|
||||
":" <> right -> right
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp printable?([c|cs]) when is_integer(c) and c in 32..126, do: printable?(cs)
|
||||
defp printable?([?\n|cs]), do: printable?(cs)
|
||||
defp printable?([?\r|cs]), do: printable?(cs)
|
||||
defp printable?([?\t|cs]), do: printable?(cs)
|
||||
defp printable?([?\v|cs]), do: printable?(cs)
|
||||
defp printable?([?\b|cs]), do: printable?(cs)
|
||||
defp printable?([?\f|cs]), do: printable?(cs)
|
||||
defp printable?([?\e|cs]), do: printable?(cs)
|
||||
defp printable?([?\a|cs]), do: printable?(cs)
|
||||
defp printable?([]), do: true
|
||||
defp printable?(_), do: false
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Tuple do
|
||||
def inspect({}, _opts), do: "{}"
|
||||
|
||||
def inspect(tuple, opts) do
|
||||
if opts.records do
|
||||
record_inspect(tuple, opts)
|
||||
else
|
||||
surround_many("{", tuple_to_list(tuple), "}", opts.limit, &to_doc(&1, opts))
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp record_inspect(record, opts) do
|
||||
[name|tail] = tuple_to_list(record)
|
||||
|
||||
if is_atom(name) && (fields = record_fields(name)) && (length(fields) == size(record) - 1) do
|
||||
surround_record(name, fields, tail, opts)
|
||||
else
|
||||
surround_many("{", [name|tail], "}", opts.limit, &to_doc(&1, opts))
|
||||
end
|
||||
end
|
||||
|
||||
defp record_fields(name) do
|
||||
case atom_to_binary(name) do
|
||||
"Elixir." <> _ ->
|
||||
try do
|
||||
name.__record__(:fields)
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp surround_record(name, fields, tail, opts) do
|
||||
concat(
|
||||
Inspect.Atom.inspect(name, opts),
|
||||
surround_many("[", zip_fields(fields, tail), "]", opts.limit, &keyword(&1, opts))
|
||||
)
|
||||
end
|
||||
|
||||
defp zip_fields([{key, _}|tk], [value|tv]) do
|
||||
case atom_to_binary(key) do
|
||||
"_" <> _ -> zip_fields(tk, tv)
|
||||
key -> [{key, value}|zip_fields(tk, tv)]
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_fields([], []) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp keyword({k, v}, opts) do
|
||||
concat(k <> ": ", to_doc(v, opts))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Map do
|
||||
def inspect(map, opts) do
|
||||
inspect(map, "", opts)
|
||||
end
|
||||
|
||||
def inspect(map, name, opts) do
|
||||
map = :maps.to_list(map)
|
||||
surround_many("%" <> name <> "{", map, "}", opts.limit, traverse_fun(map, opts))
|
||||
end
|
||||
|
||||
defp traverse_fun(list, opts) do
|
||||
if Inspect.List.keyword?(list) do
|
||||
&Inspect.List.keyword(&1, opts)
|
||||
else
|
||||
&to_map(&1, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp to_map({key, value}, opts) do
|
||||
concat(
|
||||
concat(to_doc(key, opts), " => "),
|
||||
to_doc(value, opts)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Integer do
|
||||
def inspect(thing, _opts) do
|
||||
integer_to_binary(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Float do
|
||||
def inspect(thing, _opts) do
|
||||
iodata_to_binary(:io_lib_format.fwrite_g(thing))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Regex do
|
||||
def inspect(regex, opts) do
|
||||
concat ["~r", to_doc(regex.source, opts), regex.opts]
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Function do
|
||||
def inspect(function, _opts) do
|
||||
fun_info = :erlang.fun_info(function)
|
||||
mod = fun_info[:module]
|
||||
|
||||
if fun_info[:type] == :external and fun_info[:env] == [] do
|
||||
"&#{Inspect.Atom.inspect(mod)}.#{fun_info[:name]}/#{fun_info[:arity]}"
|
||||
else
|
||||
case atom_to_list(mod) do
|
||||
'elixir_compiler_' ++ _ ->
|
||||
if function_exported?(mod, :__RELATIVE__, 0) do
|
||||
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
|
||||
else
|
||||
default_inspect(mod, fun_info)
|
||||
end
|
||||
_ ->
|
||||
default_inspect(mod, fun_info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp default_inspect(mod, fun_info) do
|
||||
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in " <>
|
||||
"#{Inspect.Atom.inspect(mod)}#{extract_name(fun_info[:name])}>"
|
||||
end
|
||||
|
||||
defp extract_name([]) do
|
||||
""
|
||||
end
|
||||
|
||||
defp extract_name(name) do
|
||||
name = atom_to_binary(name)
|
||||
case :binary.split(name, "-", [:global]) do
|
||||
["", name | _] -> "." <> name
|
||||
_ -> "." <> name
|
||||
end
|
||||
end
|
||||
|
||||
defp uniq(fun_info) do
|
||||
integer_to_binary(fun_info[:new_index]) <> "." <>
|
||||
integer_to_binary(fun_info[:uniq])
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: PID do
|
||||
def inspect(pid, _opts) do
|
||||
"#PID" <> iodata_to_binary(:erlang.pid_to_list(pid))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Port do
|
||||
def inspect(port, _opts) do
|
||||
iodata_to_binary :erlang.port_to_list(port)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Reference do
|
||||
def inspect(ref, _opts) do
|
||||
'#Ref' ++ rest = :erlang.ref_to_list(ref)
|
||||
"#Reference" <> iodata_to_binary(rest)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
def inspect(%{__struct__: struct} = map, opts) do
|
||||
try do
|
||||
struct.__struct__
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect(map, opts)
|
||||
else
|
||||
dunder ->
|
||||
if :maps.keys(dunder) == :maps.keys(map) do
|
||||
Inspect.Map.inspect(:maps.remove(:__struct__, map),
|
||||
Inspect.Atom.inspect(struct, opts), opts)
|
||||
else
|
||||
Inspect.Map.inspect(map, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: HashDict do
|
||||
def inspect(dict, opts) do
|
||||
concat ["#HashDict<", Inspect.List.inspect(HashDict.to_list(dict), opts), ">"]
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: HashSet do
|
||||
def inspect(set, opts) do
|
||||
concat ["#HashSet<", Inspect.List.inspect(HashSet.to_list(set), opts), ">"]
|
||||
end
|
||||
end
|
||||
@@ -1,507 +0,0 @@
|
||||
defmodule Inspect.Opts do
|
||||
@moduledoc """
|
||||
Defines the Inspect.Opts used by the Inspect protocol.
|
||||
|
||||
The following fields are available:
|
||||
|
||||
* `:structs` - when false, structs are not formatted by the inspect protocol,
|
||||
they are instead printed as maps, defaults to true;
|
||||
|
||||
* `:binaries` - when `:as_strings` all binaries will be printed as strings,
|
||||
non-printable bytes will be escaped; when `:as_binaries` all
|
||||
binaries will be printed in bit syntax; when the default
|
||||
`:infer`, the binary will be printed as a string if it is
|
||||
printable, otherwise in bit syntax;
|
||||
|
||||
* `:char_lists` - when `:as_char_lists` all lists will be printed as char lists,
|
||||
non-printable elements will be escaped; when `:as_lists` all
|
||||
lists will be printed as lists; when the default `:infer`, the
|
||||
list will be printed as a char list if it is printable,
|
||||
otherwise as list;
|
||||
|
||||
* `:limit` - limits the number of items that are printed for tuples, bitstrings,
|
||||
and lists, does not apply to strings nor char lists, defaults to 50;
|
||||
|
||||
* `:pretty` - if set to true enables pretty printing, defaults to false;
|
||||
|
||||
* `:width` - defaults to the 80 characters;
|
||||
"""
|
||||
|
||||
defstruct structs: true :: boolean,
|
||||
binaries: :infer :: :infer | :as_binaries | :as_strings,
|
||||
char_lists: :infer :: :infer | :as_lists | :as_char_lists,
|
||||
limit: 50 :: pos_integer,
|
||||
width: 80 :: pos_integer | :infinity,
|
||||
pretty: false :: boolean,
|
||||
records: true :: boolean
|
||||
end
|
||||
|
||||
defmodule Inspect.Algebra do
|
||||
@moduledoc ~S"""
|
||||
A set of functions for creating and manipulating algebra
|
||||
documents, as described in ["Strictly Pretty" (2000) by Christian Lindig][0].
|
||||
|
||||
An algebra document is represented by an `Inspect.Algebra` node
|
||||
or a regular string.
|
||||
|
||||
iex> Inspect.Algebra.empty
|
||||
:doc_nil
|
||||
|
||||
iex> "foo"
|
||||
"foo"
|
||||
|
||||
With the functions in this module, we can concatenate different
|
||||
elements together and render them:
|
||||
|
||||
iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty, "foo")
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"foo"
|
||||
|
||||
The functions `nest/2`, `space/2` and `line/2` help you put the
|
||||
document together into a rigid structure. However, the document
|
||||
algebra gets interesting when using functions like `break/2`, which
|
||||
converts the given string into a line break depending on how much space
|
||||
there is to print. Let's glue two docs together with a break and then
|
||||
render it:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue("a", " ", "b")
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"a b"
|
||||
|
||||
Notice the break was represented as is, because we haven't reached
|
||||
a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
|
||||
iex> Inspect.Algebra.pretty(doc, 10)
|
||||
"aaaaaaaaaaaaaaaaaaaa\nb"
|
||||
|
||||
Finally, this module also contains Elixir related functions, a bit
|
||||
tied to Elixir formatting, namely `surround/3` and `surround_many/5`.
|
||||
|
||||
## Implementation details
|
||||
|
||||
The original Haskell implementation of the algorithm by [Wadler][1]
|
||||
relies on lazy evaluation to unfold document groups on two alternatives:
|
||||
`:flat` (breaks as spaces) and `:break` (breaks as newlines).
|
||||
Implementing the same logic in a strict language such as Elixir leads
|
||||
to an exponential growth of possible documents, unless document groups
|
||||
are encoded explictly as `:flat` or `:break`. Those groups are then reduced
|
||||
to a simple document, where the layout is already decided, per [Lindig][0].
|
||||
|
||||
This implementation slightly changes the semantic of Lindig's algorithm
|
||||
to allow elements that belong to the same group to be printed together
|
||||
in the same line, even if they do not fit the line fully. This was achieved
|
||||
by changing `:break` to mean a possible break and `:flat` to force a flat
|
||||
structure. Then deciding if a break works as a newline is just a matter
|
||||
of checking if we have enough space until the next break that is not
|
||||
inside a group (which is still flat).
|
||||
|
||||
Custom pretty printers can be implemented using the documents returned
|
||||
by this module and by providing their own rendering functions.
|
||||
|
||||
[0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
|
||||
[1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
|
||||
|
||||
"""
|
||||
|
||||
@surround_separator ","
|
||||
@tail_separator " |"
|
||||
@newline "\n"
|
||||
@nesting 1
|
||||
@break " "
|
||||
|
||||
# Functional interface to `doc` records
|
||||
|
||||
@type t :: :doc_nil | :doc_line | doc_cons | doc_nest | doc_break | doc_group | binary
|
||||
|
||||
require Record
|
||||
|
||||
@typep doc_cons :: {:doc_cons, t, t}
|
||||
Record.defrecordp :doc_cons, left: :doc_nil, right: :doc_nil
|
||||
|
||||
@typep doc_nest :: {:doc_nest, non_neg_integer, t}
|
||||
Record.defrecordp :doc_nest, indent: 1, doc: :doc_nil
|
||||
|
||||
@typep doc_break :: {:doc_break, binary}
|
||||
Record.defrecordp :doc_break, str: " "
|
||||
|
||||
@typep doc_group :: {:doc_group, t}
|
||||
Record.defrecordp :doc_group, doc: :doc_nil
|
||||
|
||||
defmacrop is_doc(doc) do
|
||||
if Macro.Env.in_guard?(__CALLER__) do
|
||||
do_is_doc(doc)
|
||||
else
|
||||
var = quote do: doc
|
||||
quote do
|
||||
unquote(var) = unquote(doc)
|
||||
unquote(do_is_doc(var))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_is_doc(doc) do
|
||||
quote do
|
||||
is_binary(unquote(doc)) or
|
||||
unquote(doc) in [:doc_nil, :doc_line] or
|
||||
(is_tuple(unquote(doc)) and
|
||||
elem(unquote(doc), 0) in [:doc_cons, :doc_nest, :doc_break, :doc_group])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an Elixir structure to an algebra document
|
||||
according to the inspect protocol.
|
||||
"""
|
||||
@spec to_doc(any, Inspect.Opts.t) :: t
|
||||
def to_doc(arg, %Inspect.Opts{} = opts) when is_record(arg) do
|
||||
# IO.write :stderr, "inspect records is deprecated (just remove this code when protocols+records are removed)\n#{Exception.format_stacktrace}"
|
||||
if opts.records do
|
||||
try do
|
||||
Inspect.inspect(arg, opts)
|
||||
rescue
|
||||
e ->
|
||||
res = Inspect.Tuple.inspect(arg, opts)
|
||||
raise ArgumentError, message:
|
||||
"Got #{inspect e.__record__(:name)} with message " <>
|
||||
"#{Exception.message(e)} while inspecting #{pretty(res, opts.width)}"
|
||||
end
|
||||
else
|
||||
Inspect.Tuple.inspect(arg, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def to_doc(%{__struct__: struct} = map, %Inspect.Opts{} = opts) when is_atom(struct) do
|
||||
if opts.structs do
|
||||
try do
|
||||
Inspect.inspect(map, opts)
|
||||
rescue
|
||||
e ->
|
||||
res = Inspect.Map.inspect(map, opts)
|
||||
raise ArgumentError, message:
|
||||
"Got #{inspect e.__record__(:name)} with message " <>
|
||||
"\"#{Exception.message(e)}\" while inspecting #{pretty(res, opts.width)}"
|
||||
end
|
||||
else
|
||||
Inspect.Map.inspect(map, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def to_doc(arg, %Inspect.Opts{} = opts) do
|
||||
Inspect.inspect(arg, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `:doc_nil` which is a document entity used to represent
|
||||
nothingness. Takes no arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Inspect.Algebra.empty
|
||||
:doc_nil
|
||||
|
||||
"""
|
||||
@spec empty() :: :doc_nil
|
||||
def empty, do: :doc_nil
|
||||
|
||||
@doc """
|
||||
Concatenates two document entities. Takes two arguments:
|
||||
left doc and right doc. Returns a DocCons doc
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.concat "Tasteless", "Artosis"
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"TastelessArtosis"
|
||||
|
||||
"""
|
||||
@spec concat(t, t) :: doc_cons
|
||||
def concat(x, y) when is_doc(x) and is_doc(y) do
|
||||
doc_cons(left: x, right: y)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a list of documents.
|
||||
"""
|
||||
@spec concat([t]) :: doc_cons
|
||||
def concat(docs) do
|
||||
folddoc(docs, &concat(&1, &2))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Nests document entity `x` positions deep. Nesting will be
|
||||
appended to the line breaks.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.nest(Inspect.Algebra.concat(Inspect.Algebra.break, "6"), 5)
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
" 6"
|
||||
|
||||
"""
|
||||
@spec nest(t, non_neg_integer) :: doc_nest
|
||||
def nest(x, 0) when is_doc(x) do
|
||||
x
|
||||
end
|
||||
|
||||
def nest(x, i) when is_doc(x) and is_integer(i) do
|
||||
doc_nest(indent: i, doc: x)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Document entity representing a break. This break can
|
||||
be rendered as a linebreak or as spaces, depending on the
|
||||
`mode` of the chosen layout or the provided separator.
|
||||
|
||||
## Examples
|
||||
|
||||
Let's glue two docs together with a break and then render it:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue("a", " ", "b")
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"a b"
|
||||
|
||||
Notice the break was represented as is, because we haven't reached
|
||||
a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
|
||||
iex> Inspect.Algebra.pretty(doc, 10)
|
||||
"aaaaaaaaaaaaaaaaaaaa\nb"
|
||||
|
||||
"""
|
||||
@spec break(binary) :: doc_break
|
||||
def break(s) when is_binary(s), do: doc_break(str: s)
|
||||
|
||||
@spec break() :: doc_break
|
||||
def break(), do: doc_break(str: @break)
|
||||
|
||||
@doc """
|
||||
Inserts a break between two docs. See `break/1` for more info.
|
||||
"""
|
||||
@spec glue(t, t) :: doc_cons
|
||||
def glue(x, y), do: concat(x, concat(break, y))
|
||||
|
||||
@doc """
|
||||
Inserts a break, passed as the second argument, between two docs,
|
||||
the first and the third arguments.
|
||||
"""
|
||||
@spec glue(t, binary, t) :: doc_cons
|
||||
def glue(x, g, y) when is_binary(g), do: concat(x, concat(break(g), y))
|
||||
|
||||
@doc ~S"""
|
||||
Returns a group containing the specified document.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.group(
|
||||
...> Inspect.Algebra.concat(
|
||||
...> Inspect.Algebra.group(
|
||||
...> Inspect.Algebra.concat(
|
||||
...> "Hello,",
|
||||
...> Inspect.Algebra.concat(
|
||||
...> Inspect.Algebra.break,
|
||||
...> "A"
|
||||
...> )
|
||||
...> )
|
||||
...> ),
|
||||
...> Inspect.Algebra.concat(
|
||||
...> Inspect.Algebra.break,
|
||||
...> "B"
|
||||
...> )
|
||||
...> ))
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"Hello, A B"
|
||||
iex> Inspect.Algebra.pretty(doc, 6)
|
||||
"Hello,\nA B"
|
||||
|
||||
"""
|
||||
@spec group(t) :: doc_group
|
||||
def group(d) when is_doc(d) do
|
||||
doc_group(doc: d)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inserts a mandatory single space between two document entities.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.space "Hughes", "Wadler"
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"Hughes Wadler"
|
||||
|
||||
"""
|
||||
@spec space(t, t) :: doc_cons
|
||||
def space(x, y), do: concat(x, concat(" ", y))
|
||||
|
||||
@doc ~S"""
|
||||
Inserts a mandatory linebreak between two document entities.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.line "Hughes", "Wadler"
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"Hughes\nWadler"
|
||||
|
||||
"""
|
||||
@spec line(t, t) :: doc_cons
|
||||
def line(x, y), do: concat(x, concat(:doc_line, y))
|
||||
|
||||
@doc """
|
||||
Folds a list of document entities into a document entity
|
||||
using a function that is passed as the first argument.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = ["A", "B"]
|
||||
iex> doc = Inspect.Algebra.folddoc(doc, fn(x,y) ->
|
||||
...> Inspect.Algebra.concat [x, "!", y]
|
||||
...> end)
|
||||
iex> Inspect.Algebra.pretty(doc, 80)
|
||||
"A!B"
|
||||
|
||||
"""
|
||||
@spec folddoc([t], ((t, t) -> t)) :: t
|
||||
def folddoc([], _), do: empty
|
||||
def folddoc([doc], _), do: doc
|
||||
def folddoc([d|ds], f), do: f.(d, folddoc(ds, f))
|
||||
|
||||
# Elixir conveniences
|
||||
|
||||
@doc ~S"""
|
||||
Surrounds a document with characters.
|
||||
|
||||
Puts the document between left and right enclosing and nesting it.
|
||||
The document is marked as a group, to show the maximum as possible
|
||||
concisely together.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.surround "[", Inspect.Algebra.glue("a", "b"), "]"
|
||||
iex> Inspect.Algebra.pretty(doc, 3)
|
||||
"[a\n b]"
|
||||
|
||||
"""
|
||||
@spec surround(binary, t, binary) :: t
|
||||
def surround(left, doc, right) do
|
||||
group concat left, concat(nest(doc, @nesting), right)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Maps and glues a collection of items together using the given separator
|
||||
and surrounds them. A limit can be passed which, once reached, stops
|
||||
gluing and outputs "..." instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", :infinity, &integer_to_binary(&1))
|
||||
iex> Inspect.Algebra.pretty(doc, 5)
|
||||
"[1,\n 2,\n 3,\n 4,\n 5]"
|
||||
|
||||
iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", 3, &integer_to_binary(&1))
|
||||
iex> Inspect.Algebra.pretty(doc, 20)
|
||||
"[1, 2, 3, ...]"
|
||||
|
||||
iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", 3, &integer_to_binary(&1), "!")
|
||||
iex> Inspect.Algebra.pretty(doc, 20)
|
||||
"[1! 2! 3! ...]"
|
||||
|
||||
"""
|
||||
@spec surround_many(binary, [any], binary, integer | :infinity, (term -> t), binary) :: t
|
||||
def surround_many(left, docs, right, limit, fun, separator \\ @surround_separator)
|
||||
|
||||
def surround_many(left, [], right, _, _fun, _) do
|
||||
concat(left, right)
|
||||
end
|
||||
|
||||
def surround_many(left, docs, right, limit, fun, sep) do
|
||||
surround(left, surround_many(docs, limit, fun, sep), right)
|
||||
end
|
||||
|
||||
defp surround_many(_, 0, _fun, _sep) do
|
||||
"..."
|
||||
end
|
||||
|
||||
defp surround_many([h], _limit, fun, _sep) do
|
||||
fun.(h)
|
||||
end
|
||||
|
||||
defp surround_many([h|t], limit, fun, sep) when is_list(t) do
|
||||
glue(
|
||||
concat(fun.(h), sep),
|
||||
surround_many(t, decrement(limit), fun, sep)
|
||||
)
|
||||
end
|
||||
|
||||
defp surround_many([h|t], _limit, fun, _sep) do
|
||||
glue(
|
||||
concat(fun.(h), @tail_separator),
|
||||
fun.(t)
|
||||
)
|
||||
end
|
||||
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
|
||||
@doc """
|
||||
The pretty printing function.
|
||||
|
||||
Takes the maximum width and a document to print as its arguments
|
||||
and returns the string representation of the best layout for the
|
||||
document to fit in the given width.
|
||||
"""
|
||||
@spec pretty(t, non_neg_integer | :infinity) :: binary
|
||||
def pretty(d, w) do
|
||||
sdoc = format w, 0, [{0, default_mode(w), doc_group(doc: d)}]
|
||||
render(sdoc)
|
||||
end
|
||||
|
||||
defp default_mode(:infinity), do: :flat
|
||||
defp default_mode(_), do: :break
|
||||
|
||||
# Rendering and internal helpers
|
||||
|
||||
# Record representing the document mode to be rendered: flat or broken
|
||||
@typep mode :: :flat | :break
|
||||
|
||||
@doc false
|
||||
@spec fits?(integer, [{integer, mode, t}]) :: boolean
|
||||
def fits?(w, _) when w < 0, do: false
|
||||
def fits?(_, []), do: true
|
||||
def fits?(_, [{_, _, :doc_line} | _]), do: true
|
||||
def fits?(w, [{_, _, :doc_nil} | t]), do: fits?(w, t)
|
||||
def fits?(w, [{i, m, doc_cons(left: x, right: y)} | t]), do: fits?(w, [{i, m, x} | [{i, m, y} | t]])
|
||||
def fits?(w, [{i, m, doc_nest(indent: j, doc: x)} | t]), do: fits?(w, [{i + j, m, x} | t])
|
||||
def fits?(w, [{i, _, doc_group(doc: x)} | t]), do: fits?(w, [{i, :flat, x} | t])
|
||||
def fits?(w, [{_, _, s} | t]) when is_binary(s), do: fits?((w - byte_size s), t)
|
||||
def fits?(w, [{_, :flat, doc_break(str: s)} | t]), do: fits?((w - byte_size s), t)
|
||||
def fits?(_, [{_, :break, doc_break(str: _)} | _]), do: true
|
||||
|
||||
@doc false
|
||||
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
|
||||
def format(_, _, []), do: []
|
||||
def format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
|
||||
def format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
|
||||
def format(w, k, [{i, m, doc_cons(left: x, right: y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
|
||||
def format(w, k, [{i, m, doc_nest(indent: j, doc: x)} | t]), do: format(w, k, [{i + j, m, x} | t])
|
||||
def format(w, k, [{i, m, doc_group(doc: x)} | t]), do: format(w, k, [{i, m, x} | t])
|
||||
def format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, (k + byte_size s), t)]
|
||||
def format(w, k, [{_, :flat, doc_break(str: s)} | t]), do: [s | format(w, (k + byte_size s), t)]
|
||||
def format(w, k, [{i, :break, doc_break(str: s)} | t]) do
|
||||
k = k + byte_size(s)
|
||||
|
||||
if w == :infinity or fits?(w - k, t) do
|
||||
[s | format(w, k, t)]
|
||||
else
|
||||
[indent(i) | format(w, i, t)]
|
||||
end
|
||||
end
|
||||
|
||||
defp indent(0), do: @newline
|
||||
defp indent(i), do: @newline <> :binary.copy(" ", i)
|
||||
|
||||
@doc false
|
||||
@spec render([binary]) :: binary
|
||||
def render(sdoc) do
|
||||
iodata_to_binary sdoc
|
||||
end
|
||||
end
|
||||
@@ -1,72 +0,0 @@
|
||||
defmodule Integer do
|
||||
@moduledoc """
|
||||
Functions for working with integers.
|
||||
"""
|
||||
|
||||
import Bitwise
|
||||
|
||||
@doc """
|
||||
Determines if an integer is odd.
|
||||
|
||||
Returns `true` if `n` is an odd number, otherwise `false`.
|
||||
Implemented as a macro so it is allowed in guard clauses.
|
||||
"""
|
||||
defmacro odd?(n) do
|
||||
quote do: (unquote(n) &&& 1) == 1
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines if an integer is even.
|
||||
|
||||
Returns `true` if `n` is an even number, otherwise `false`.
|
||||
Implemented as a macro so it is allowed in guard clauses.
|
||||
"""
|
||||
defmacro even?(n) do
|
||||
quote do: (unquote(n) &&& 1) == 0
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a binary to an integer.
|
||||
|
||||
If successful, returns a tuple of the form `{integer, remainder_of_binary}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.parse("34")
|
||||
{34,""}
|
||||
|
||||
iex> Integer.parse("34.5")
|
||||
{34,".5"}
|
||||
|
||||
iex> Integer.parse("three")
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec parse(binary) :: {integer, binary} | :error
|
||||
def parse(<< ?-, bin :: binary >>) do
|
||||
case do_parse(bin) do
|
||||
:error -> :error
|
||||
{number, remainder} -> {-number, remainder}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(<< ?+, bin :: binary >>) do
|
||||
do_parse(bin)
|
||||
end
|
||||
|
||||
def parse(bin) when is_binary(bin) do
|
||||
do_parse(bin)
|
||||
end
|
||||
|
||||
defp do_parse(<< char, bin :: binary >>) when char in ?0..?9, do: do_parse(bin, char - ?0)
|
||||
defp do_parse(_), do: :error
|
||||
|
||||
defp do_parse(<< char, rest :: binary >>, acc) when char in ?0..?9 do
|
||||
do_parse rest, 10 * acc + (char - ?0)
|
||||
end
|
||||
|
||||
defp do_parse(bitstring, acc) do
|
||||
{acc, bitstring}
|
||||
end
|
||||
end
|
||||
@@ -1,367 +0,0 @@
|
||||
defexception IO.StreamError, [:reason, :message] do
|
||||
def exception(opts) do
|
||||
reason = opts[:reason]
|
||||
formatted = iodata_to_binary(:file.format_error(reason))
|
||||
IO.StreamError[message: "error during streaming: #{formatted}", reason: reason]
|
||||
end
|
||||
end
|
||||
|
||||
defmodule IO.Stream do
|
||||
@moduledoc """
|
||||
Defines a `IO.Stream` struct returned by `IO.stream/2` and `IO.binstream/2`.
|
||||
|
||||
The following fields are public:
|
||||
|
||||
* `device` - the IO device
|
||||
* `raw` - a boolean indicating if bin functions should be used
|
||||
* `line_or_bytes` - if reading should read lines or a given amount of bytes
|
||||
|
||||
"""
|
||||
|
||||
defstruct device: nil, raw: true, line_or_bytes: :line
|
||||
|
||||
defimpl Collectable do
|
||||
def empty(stream) do
|
||||
stream
|
||||
end
|
||||
|
||||
def into(%{device: device, raw: raw} = stream) do
|
||||
{:ok, into(stream, device, raw)}
|
||||
end
|
||||
|
||||
defp into(stream, device, raw) do
|
||||
fn
|
||||
:ok, {:cont, x} ->
|
||||
case raw do
|
||||
true -> IO.binwrite(device, x)
|
||||
false -> IO.write(device, x)
|
||||
end
|
||||
:ok, _ -> stream
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable do
|
||||
def reduce(%{device: device, raw: raw, line_or_bytes: line_or_bytes}, acc, fun) do
|
||||
next_fun =
|
||||
case raw do
|
||||
true -> &IO.each_binstream(&1, line_or_bytes)
|
||||
false -> &IO.each_stream(&1, line_or_bytes)
|
||||
end
|
||||
Stream.unfold(device, next_fun).(acc, fun)
|
||||
end
|
||||
|
||||
def count(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
def member?(_stream, _term) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule IO do
|
||||
@moduledoc """
|
||||
Functions handling IO.
|
||||
|
||||
Many functions in this module expects an IO device as argument.
|
||||
An IO device must be a pid or an atom representing a process.
|
||||
For convenience, Elixir provides `:stdio` and `:stderr` as
|
||||
shortcuts to Erlang's `:standard_io` and `:standard_error`.
|
||||
|
||||
The majority of the functions expect char data, i.e. strings or
|
||||
lists of characters and strings. In case another type is given,
|
||||
it will do a conversion to string via the `String.Chars` protocol
|
||||
(as shown in typespecs).
|
||||
|
||||
The functions starting with `bin*` expects iodata as argument,
|
||||
i.e. binaries or lists of bytes and binaries.
|
||||
|
||||
## IO devices
|
||||
|
||||
An IO device may be an atom or a pid. In case it is an atom,
|
||||
the atom must be the name of a registered process. However,
|
||||
there are three exceptions for this rule:
|
||||
|
||||
* `:standard_io` - when the `:standard_io` atom is given,
|
||||
it is treated as a shortcut for `Process.group_leader`
|
||||
|
||||
* `:stdio` - is a shortcut for `:standard_io`
|
||||
|
||||
* `:stderr` - is a shortcut for `:standard_error`
|
||||
|
||||
"""
|
||||
|
||||
@type device :: atom | pid
|
||||
@type nodata :: {:error, term} | :eof
|
||||
|
||||
import :erlang, only: [group_leader: 0]
|
||||
|
||||
defmacrop is_iodata(data) do
|
||||
quote do
|
||||
is_list(unquote(data)) or is_binary(unquote(data))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads `count` characters from the IO device or until
|
||||
the end of the line if `:line` is given. It returns:
|
||||
|
||||
* `data` - The input characters.
|
||||
|
||||
* `:eof` - End of file was encountered.
|
||||
|
||||
* `{:error, reason}` - Other (rare) error condition,
|
||||
for instance `{:error, :estale}` if reading from an
|
||||
NFS file system.
|
||||
"""
|
||||
@spec read(device, :line | non_neg_integer) :: char_data | nodata
|
||||
def read(device \\ group_leader, chars_or_line)
|
||||
|
||||
def read(device, :line) do
|
||||
:io.get_line(map_dev(device), '')
|
||||
end
|
||||
|
||||
def read(device, count) when count >= 0 do
|
||||
:io.get_chars(map_dev(device), '', count)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads `count` bytes from the IO device or until
|
||||
the end of the line if `:line` is given. It returns:
|
||||
|
||||
* `data` - The input characters.
|
||||
|
||||
* `:eof` - End of file was encountered.
|
||||
|
||||
* `{:error, reason}` - Other (rare) error condition,
|
||||
for instance `{:error, :estale}` if reading from an
|
||||
NFS file system.
|
||||
"""
|
||||
@spec binread(device, :line | non_neg_integer) :: iodata | nodata
|
||||
def binread(device \\ group_leader, chars_or_line)
|
||||
|
||||
def binread(device, :line) do
|
||||
case :file.read_line(map_dev(device)) do
|
||||
{:ok, data} -> data
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def binread(device, count) when count >= 0 do
|
||||
case :file.read(map_dev(device), count) do
|
||||
{:ok, data} -> data
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes the given argument to the given device.
|
||||
|
||||
By default the device is the standard output.
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.write "sample"
|
||||
#=> "sample"
|
||||
|
||||
IO.write :stderr, "error"
|
||||
#=> "error"
|
||||
|
||||
"""
|
||||
@spec write(device, char_data | String.Chars.t) :: :ok
|
||||
def write(device \\ group_leader(), item) do
|
||||
:io.put_chars map_dev(device), to_char_data(item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes the given argument to the given device
|
||||
as a binary, no unicode conversion happens.
|
||||
|
||||
Check `write/2` for more information.
|
||||
"""
|
||||
@spec binwrite(device, iodata) :: :ok | {:error, term}
|
||||
def binwrite(device \\ group_leader(), item) when is_iodata(item) do
|
||||
:file.write map_dev(device), item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes the argument to the device, similar to `write/2`,
|
||||
but adds a newline at the end. The argument is expected
|
||||
to be a char_data.
|
||||
"""
|
||||
@spec puts(device, char_data | String.Chars.t) :: :ok
|
||||
def puts(device \\ group_leader(), item) do
|
||||
erl_dev = map_dev(device)
|
||||
:io.put_chars erl_dev, [to_char_data(item), ?\n]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inspects and writes the given argument to the device.
|
||||
|
||||
It sets by default pretty printing to true and returns
|
||||
the item itself.
|
||||
|
||||
Note this function does not use the IO device width
|
||||
because some IO devices does not implement the
|
||||
appropriate functions. Setting the width must be done
|
||||
explicitly by passing the `:width` option.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.inspect Process.list
|
||||
|
||||
"""
|
||||
@spec inspect(term, Keyword.t) :: term
|
||||
def inspect(item, opts \\ []) do
|
||||
inspect group_leader(), item, opts
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inspects the item with options using the given device.
|
||||
"""
|
||||
@spec inspect(device, term, Keyword.t) :: term
|
||||
def inspect(device, item, opts) when is_list(opts) do
|
||||
opts = Keyword.put_new(opts, :pretty, true)
|
||||
puts device, Kernel.inspect(item, opts)
|
||||
item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from the io device. If the
|
||||
io device is a unicode device, `count` implies
|
||||
the number of unicode codepoints to be retrieved.
|
||||
Otherwise, `count` is the number of raw bytes to be retrieved.
|
||||
It returns:
|
||||
|
||||
* `data` - The input characters.
|
||||
|
||||
* `:eof` - End of file was encountered.
|
||||
|
||||
* `{:error, reason}` - Other (rare) error condition,
|
||||
for instance `{:error, :estale}` if reading from an
|
||||
NFS file system.
|
||||
"""
|
||||
@spec getn(char_data | String.Chars.t, pos_integer) :: char_data | nodata
|
||||
@spec getn(device, char_data | String.Chars.t) :: char_data | nodata
|
||||
def getn(prompt, count \\ 1)
|
||||
|
||||
def getn(prompt, count) when is_integer(count) do
|
||||
getn(group_leader, prompt, count)
|
||||
end
|
||||
|
||||
def getn(device, prompt) do
|
||||
getn(device, prompt, 1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from the io device. If the
|
||||
io device is a unicode device, `count` implies
|
||||
the number of unicode codepoints to be retrieved.
|
||||
Otherwise, `count` is the number of raw bytes to be retrieved.
|
||||
"""
|
||||
@spec getn(device, char_data | String.Chars.t, pos_integer) :: char_data | nodata
|
||||
def getn(device, prompt, count) do
|
||||
:io.get_chars(map_dev(device), to_char_data(prompt), count)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads a line from the IO device. It returns:
|
||||
|
||||
* `data` - The characters in the line terminated
|
||||
by a LF (or end of file).
|
||||
|
||||
* `:eof` - End of file was encountered.
|
||||
|
||||
* `{:error, reason}` - Other (rare) error condition,
|
||||
for instance `{:error, :estale}` if reading from an
|
||||
NFS file system.
|
||||
"""
|
||||
@spec gets(device, char_data | String.Chars.t) :: char_data | nodata
|
||||
def gets(device \\ group_leader(), prompt) do
|
||||
:io.get_line(map_dev(device), to_char_data(prompt))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the io device into a `IO.Stream`.
|
||||
|
||||
An `IO.Stream` implements both `Enumerable` and
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The device is iterated line by line if `:line` is given or
|
||||
by a given number of codepoints.
|
||||
|
||||
This reads the IO as utf-8. Check out
|
||||
`IO.binstream/2` to handle the IO as a raw binary.
|
||||
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
|
||||
## Examples
|
||||
|
||||
Here is an example on how we mimic an echo server
|
||||
from the command line:
|
||||
|
||||
Enum.each IO.stream(:stdio, :line), &IO.write(&1)
|
||||
|
||||
"""
|
||||
@spec stream(device, :line | pos_integer) :: Enumerable.t
|
||||
def stream(device, line_or_codepoints) do
|
||||
%IO.Stream{device: map_dev(device), raw: false, line_or_bytes: line_or_codepoints}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the IO device into a `IO.Stream`.
|
||||
|
||||
An `IO.Stream` implements both `Enumerable` and
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The device is iterated line by line or by a number of bytes.
|
||||
This reads the IO device as a raw binary.
|
||||
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
"""
|
||||
@spec binstream(device, :line | pos_integer) :: Enumerable.t
|
||||
def binstream(device, line_or_bytes) do
|
||||
%IO.Stream{device: map_dev(device), raw: true, line_or_bytes: line_or_bytes}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def each_stream(device, what) do
|
||||
case read(device, what) do
|
||||
:eof ->
|
||||
nil
|
||||
{:error, reason} ->
|
||||
raise IO.StreamError, reason: reason
|
||||
data ->
|
||||
{data, device}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def each_binstream(device, what) do
|
||||
case binread(device, what) do
|
||||
:eof ->
|
||||
nil
|
||||
{:error, reason} ->
|
||||
raise IO.StreamError, reason: reason
|
||||
data ->
|
||||
{data, device}
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, map_dev: 1, to_char_data: 1}
|
||||
|
||||
# Map the Elixir names for standard io and error to Erlang names
|
||||
defp map_dev(:stdio), do: :standard_io
|
||||
defp map_dev(:stderr), do: :standard_error
|
||||
defp map_dev(other) when is_atom(other) or is_pid(other) or is_tuple(other), do: other
|
||||
|
||||
defp to_char_data(list) when is_list(list), do: list
|
||||
defp to_char_data(other), do: to_string(other)
|
||||
end
|
||||
@@ -1,228 +0,0 @@
|
||||
defmodule IO.ANSI.Sequence do
|
||||
@moduledoc false
|
||||
|
||||
defmacro defsequence(name, code \\ "", terminator \\ "m") do
|
||||
quote bind_quoted: [name: name, code: code, terminator: terminator] do
|
||||
def unquote(name)() do
|
||||
"\e[#{unquote(code)}#{unquote(terminator)}"
|
||||
end
|
||||
|
||||
defp escape_sequence(unquote(atom_to_list(name))) do
|
||||
unquote(name)()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule IO.ANSI do
|
||||
@moduledoc """
|
||||
Functionality to render ANSI escape sequences
|
||||
(http://en.wikipedia.org/wiki/ANSI_escape_code) — characters embedded
|
||||
in text used to control formatting, color, and other output options
|
||||
on video text terminals.
|
||||
"""
|
||||
|
||||
import IO.ANSI.Sequence
|
||||
|
||||
@doc """
|
||||
Checks whether the default I/O device is a terminal or a file.
|
||||
|
||||
Used to identify whether printing ANSI escape sequences will likely
|
||||
be displayed as intended. This is checked by sending a message to
|
||||
the group leader. In case the group leader does not support the message,
|
||||
it will likely lead to a timeout (and a slow down on execution time).
|
||||
"""
|
||||
@spec terminal? :: boolean
|
||||
@spec terminal?(:io.device) :: boolean
|
||||
def terminal?(device \\ :erlang.group_leader) do
|
||||
!match?({:win32, _}, :os.type()) and
|
||||
match?({:ok, _}, :io.columns(device))
|
||||
end
|
||||
|
||||
@doc "Resets all attributes"
|
||||
defsequence :reset, 0
|
||||
|
||||
@doc "Bright (increased intensity) or Bold"
|
||||
defsequence :bright, 1
|
||||
|
||||
@doc "Faint (decreased intensity), not widely supported"
|
||||
defsequence :faint, 2
|
||||
|
||||
@doc "Italic: on. Not widely supported. Sometimes treated as inverse."
|
||||
defsequence :italic, 3
|
||||
|
||||
@doc "Underline: Single"
|
||||
defsequence :underline, 4
|
||||
|
||||
@doc "Blink: Slow. Less than 150 per minute"
|
||||
defsequence :blink_slow, 5
|
||||
|
||||
@doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
|
||||
defsequence :blink_rapid, 6
|
||||
|
||||
@doc "Image: Negative. Swap foreground and background"
|
||||
defsequence :inverse, 7
|
||||
|
||||
@doc "Image: Negative. Swap foreground and background"
|
||||
defsequence :reverse, 7
|
||||
|
||||
@doc "Conceal. Not widely supported"
|
||||
defsequence :conceal, 8
|
||||
|
||||
@doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
|
||||
defsequence :crossed_out, 9
|
||||
|
||||
@doc "Sets primary (default) font"
|
||||
defsequence :primary_font, 10
|
||||
|
||||
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
|
||||
@doc "Sets alternative font #{font_n}"
|
||||
defsequence :"font_#{font_n}", font_n + 10
|
||||
end
|
||||
|
||||
@doc "Normal color or intensity"
|
||||
defsequence :normal, 22
|
||||
|
||||
@doc "Not italic"
|
||||
defsequence :not_italic, 23
|
||||
|
||||
@doc "Underline: None"
|
||||
defsequence :no_underline, 24
|
||||
|
||||
@doc "Blink: off"
|
||||
defsequence :blink_off, 25
|
||||
|
||||
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
||||
colors = Enum.zip(0..(length(colors)-1), colors)
|
||||
|
||||
for {code, color} <- colors do
|
||||
@doc "Sets foreground color to #{color}"
|
||||
defsequence color, code + 30
|
||||
|
||||
@doc "Sets background color to #{color}"
|
||||
defsequence :"#{color}_background", code + 40
|
||||
end
|
||||
|
||||
@doc "Default text color"
|
||||
defsequence :default_color, 39
|
||||
|
||||
@doc "Default background color"
|
||||
defsequence :default_background, 49
|
||||
|
||||
@doc "Framed"
|
||||
defsequence :framed, 51
|
||||
|
||||
@doc "Encircled"
|
||||
defsequence :encircled, 52
|
||||
|
||||
@doc "Overlined"
|
||||
defsequence :overlined, 53
|
||||
|
||||
@doc "Not framed or encircled"
|
||||
defsequence :not_framed_encircled, 54
|
||||
|
||||
@doc "Not overlined"
|
||||
defsequence :not_overlined, 55
|
||||
|
||||
@doc "Send cursor home"
|
||||
defsequence :home, "", "H"
|
||||
|
||||
@doc "Clear screen"
|
||||
defsequence :clear, "2", "J"
|
||||
|
||||
defp escape_sequence(other) do
|
||||
raise ArgumentError, message: "invalid ANSI sequence specification: #{other}"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Escapes a string by converting named ANSI sequences into actual ANSI codes.
|
||||
|
||||
The format for referring to sequences is `%{red}` and `%{red,bright}` (for
|
||||
multiple sequences).
|
||||
|
||||
It will also append a `%{reset}` to the string. If you don't want this
|
||||
behaviour, use `escape_fragment/2`.
|
||||
|
||||
An optional boolean parameter can be passed to enable or disable
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
|
||||
By default, standard output will be checked if it is a terminal capable
|
||||
of handling these sequences (using `terminal?/1` function)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.ANSI.escape("Hello %{red,bright,green}yes", true)
|
||||
"Hello \e[31m\e[1m\e[32myes\e[0m"
|
||||
|
||||
"""
|
||||
@spec escape(String.t, emit :: boolean) :: String.t
|
||||
def escape(string, emit \\ terminal?) do
|
||||
{rendered, emitted} = do_escape(string, emit, false, nil, [])
|
||||
if emitted do
|
||||
rendered <> reset
|
||||
else
|
||||
rendered
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Escapes a string by converting named ANSI sequences into actual ANSI codes.
|
||||
|
||||
The format for referring to sequences is `%{red}` and `%{red,bright}` (for
|
||||
multiple sequences).
|
||||
|
||||
An optional boolean parameter can be passed to enable or disable
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
|
||||
By default, standard output will be checked if it is a terminal capable
|
||||
of handling these sequences (using `terminal?/1` function)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.ANSI.escape_fragment("Hello %{red,bright,green}yes", true)
|
||||
"Hello \e[31m\e[1m\e[32myes"
|
||||
|
||||
iex> IO.ANSI.escape_fragment("%{reset}bye", true)
|
||||
"\e[0mbye"
|
||||
|
||||
"""
|
||||
@spec escape_fragment(String.t, emit :: boolean) :: String.t
|
||||
def escape_fragment(string, emit \\ terminal?) do
|
||||
{escaped, _emitted} = do_escape(string, emit, false, nil, [])
|
||||
escaped
|
||||
end
|
||||
|
||||
defp do_escape(<<?}, t :: binary>>, emit, emitted, buffer, acc) when is_list(buffer) do
|
||||
sequences =
|
||||
buffer
|
||||
|> Enum.reverse()
|
||||
|> :string.tokens(',')
|
||||
|> Enum.map(&(&1 |> :string.strip |> escape_sequence))
|
||||
|> Enum.reverse()
|
||||
|
||||
if emit and sequences != [] do
|
||||
do_escape(t, emit, true, nil, sequences ++ acc)
|
||||
else
|
||||
do_escape(t, emit, emitted, nil, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_escape(<<h, t :: binary>>, emit, emitted, buffer, acc) when is_list(buffer) do
|
||||
do_escape(t, emit, emitted, [h|buffer], acc)
|
||||
end
|
||||
|
||||
defp do_escape(<<>>, _emit, _emitted, buffer, _acc) when is_list(buffer) do
|
||||
buffer = iodata_to_binary Enum.reverse(buffer)
|
||||
raise ArgumentError, message: "missing } for escape fragment #{buffer}"
|
||||
end
|
||||
|
||||
defp do_escape(<<?%, ?{, t :: binary>>, emit, emitted, nil, acc) do
|
||||
do_escape(t, emit, emitted, [], acc)
|
||||
end
|
||||
|
||||
defp do_escape(<<h, t :: binary>>, emit, emitted, nil, acc) do
|
||||
do_escape(t, emit, emitted, nil, [h|acc])
|
||||
end
|
||||
|
||||
defp do_escape(<<>>, _emit, emitted, nil, acc) do
|
||||
{iodata_to_binary(Enum.reverse(acc)), emitted}
|
||||
end
|
||||
end
|
||||
@@ -1,376 +0,0 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullets [?*, ?-, ?+]
|
||||
|
||||
@doc """
|
||||
The default options used by this module.
|
||||
|
||||
The supported values are:
|
||||
|
||||
* `:enabled` - toggles coloring on and off (true)
|
||||
* `:doc_code` - code blocks (cyan, bright)
|
||||
* `:doc_inline_code` - inline code (cyan)
|
||||
* `:doc_headings` - h1 and h2 headings (yellow, bright)
|
||||
* `:doc_title` - top level heading (reverse, yellow, bright)
|
||||
* `:doc_bold` - bold text (bright)
|
||||
* `:doc_underline` - underlined text (underline)
|
||||
* `:width` - the width to format the text (80)
|
||||
|
||||
Values for the color settings are strings with
|
||||
comma-separated ANSI values.
|
||||
"""
|
||||
def default_options do
|
||||
[enabled: true,
|
||||
doc_code: "cyan,bright",
|
||||
doc_inline_code: "cyan",
|
||||
doc_headings: "yellow,bright",
|
||||
doc_title: "reverse,yellow,bright",
|
||||
doc_bold: "bright",
|
||||
doc_underline: "underline",
|
||||
width: 80]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the head of the documentation (i.e. the function signature).
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
def print_heading(heading, options \\ []) do
|
||||
IO.puts IO.ANSI.reset
|
||||
options = Keyword.merge(default_options, options)
|
||||
width = options[:width]
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = heading |> String.rjust(padding) |> String.ljust(width)
|
||||
write(:doc_title, heading, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the documentation body.
|
||||
|
||||
In addition to the priting string, takes a set of options
|
||||
defined in `default_options/1`.
|
||||
"""
|
||||
def print(doc, options \\ []) do
|
||||
options = Keyword.merge(default_options, options)
|
||||
doc
|
||||
|> String.split(["\r\n","\n"], trim: false)
|
||||
|> Enum.map(&String.rstrip/1)
|
||||
|> process("", options)
|
||||
end
|
||||
|
||||
defp process([], _indent, _options), do: nil
|
||||
|
||||
defp process(["# " <> heading | rest], _indent, options) do
|
||||
write_h1(String.strip(heading), options)
|
||||
process(rest, "", options)
|
||||
end
|
||||
|
||||
defp process(["## " <> heading | rest], _indent, options) do
|
||||
write_h2(String.strip(heading), options)
|
||||
process(rest, "", options)
|
||||
end
|
||||
|
||||
defp process(["### " <> heading | rest], indent, options) do
|
||||
write_h3(String.strip(heading), indent, options)
|
||||
process(rest, indent, options)
|
||||
end
|
||||
|
||||
defp process(["" | rest], indent, options) do
|
||||
process(rest, indent, options)
|
||||
end
|
||||
|
||||
defp process([" " <> line | rest], indent, options) do
|
||||
process_code(rest, [line], indent, options)
|
||||
end
|
||||
|
||||
defp process([line | rest], indent, options) do
|
||||
{stripped, count} = strip_spaces(line, 0)
|
||||
case stripped do
|
||||
<<bullet, ?\s, item :: binary >> when bullet in @bullets ->
|
||||
process_list(item, rest, count, indent, options)
|
||||
_ ->
|
||||
process_text(rest, [line], indent, false, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp strip_spaces(" " <> line, acc) do
|
||||
strip_spaces(line, acc + 1)
|
||||
end
|
||||
|
||||
defp strip_spaces(rest, acc) do
|
||||
{rest, acc}
|
||||
end
|
||||
|
||||
## Headings
|
||||
|
||||
defp write_h1(heading, options) do
|
||||
write_h2(String.upcase(heading), options)
|
||||
end
|
||||
|
||||
defp write_h2(heading, options) do
|
||||
write(:doc_headings, heading, options)
|
||||
end
|
||||
|
||||
defp write_h3(heading, indent, options) do
|
||||
IO.write(indent)
|
||||
write(:doc_headings, heading, options)
|
||||
end
|
||||
|
||||
## Lists
|
||||
|
||||
defp process_list(line, rest, count, indent, options) do
|
||||
IO.write indent <> "• "
|
||||
{contents, rest, done} = process_list_next(rest, count, false, [])
|
||||
process_text(contents, [line], indent <> " ", true, options)
|
||||
if done, do: IO.puts(IO.ANSI.reset)
|
||||
process(rest, indent, options)
|
||||
end
|
||||
|
||||
# Process the thing after a list item entry. It can be either:
|
||||
#
|
||||
# * Continuation of the list
|
||||
# * A nested list
|
||||
# * The end of the list
|
||||
#
|
||||
defp process_list_next([" " <> _ = line | rest], count, _done, acc) do
|
||||
case list_next(line, count) do
|
||||
:done -> {Enum.reverse(acc), [line|rest], false}
|
||||
chopped -> process_list_next(rest, count, false, [chopped|acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp process_list_next([<<bullet, ?\s, _ :: binary>> | _] = rest, _count, _done, acc) when bullet in @bullets do
|
||||
{Enum.reverse(acc), rest, false}
|
||||
end
|
||||
|
||||
defp process_list_next(["" | rest], count, _done, acc) do
|
||||
process_list_next(rest, count, true, [""|acc])
|
||||
end
|
||||
|
||||
defp process_list_next(rest, _count, done, acc) do
|
||||
{Enum.reverse(acc), rest, done}
|
||||
end
|
||||
|
||||
defp list_next(<<bullet, ?\s, _ :: binary>>, 0) when bullet in @bullets, do: :done
|
||||
defp list_next(line, 0), do: chop(line, 2)
|
||||
defp list_next(" " <> line, acc), do: list_next(line, acc - 1)
|
||||
defp list_next(line, _acc), do: line
|
||||
|
||||
defp chop(" " <> line, acc) when acc > 0, do: chop(line, acc - 1)
|
||||
defp chop(line, _acc), do: line
|
||||
|
||||
## Text (paragraphs / lists)
|
||||
|
||||
defp process_text(doc=["" | _], para, indent, from_list, options) do
|
||||
write_text(Enum.reverse(para), indent, from_list, options)
|
||||
process(doc, indent, options)
|
||||
end
|
||||
|
||||
defp process_text([], para, indent, from_list, options) do
|
||||
write_text(Enum.reverse(para), indent, from_list, options)
|
||||
end
|
||||
|
||||
defp process_text([line | rest], para, indent, true, options) do
|
||||
{stripped, count} = strip_spaces(line, 0)
|
||||
case stripped do
|
||||
<<bullet, ?\s, item :: binary>> when bullet in @bullets ->
|
||||
write_text(Enum.reverse(para), indent, true, options)
|
||||
process_list(item, rest, count, indent, options)
|
||||
_ ->
|
||||
process_text(rest, [line | para], indent, true, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp process_text([line | rest], para, indent, from_list, options) do
|
||||
process_text(rest, [line | para], indent, from_list, options)
|
||||
end
|
||||
|
||||
defp write_text(lines, indent, from_list, options) do
|
||||
lines
|
||||
|> Enum.join(" ")
|
||||
|> handle_links
|
||||
|> handle_inline(nil, [], [], options)
|
||||
|> String.split(~r{\s})
|
||||
|> write_with_wrap(options[:width] - size(indent), indent, from_list)
|
||||
|
||||
unless from_list, do: IO.puts(IO.ANSI.reset)
|
||||
end
|
||||
|
||||
## Code blocks
|
||||
|
||||
defp process_code([], code, indent, options) do
|
||||
write_code(code, indent, options)
|
||||
end
|
||||
|
||||
# Blank line between code blocks
|
||||
defp process_code([ "", " " <> line | rest ], code, indent, options) do
|
||||
process_code(rest, [line, "" | code], indent, options)
|
||||
end
|
||||
|
||||
defp process_code([ " " <> line | rest ], code, indent, options) do
|
||||
process_code(rest, [line|code], indent, options)
|
||||
end
|
||||
|
||||
defp process_code(rest, code, indent, options) do
|
||||
write_code(code, indent, options)
|
||||
process(rest, indent, options)
|
||||
end
|
||||
|
||||
defp write_code(code, indent, options) do
|
||||
write(:doc_code, "#{indent}┃ #{Enum.join(Enum.reverse(code), "\n#{indent}┃ ")}", options)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp write(style, string, options) do
|
||||
IO.puts color(style, options) <> string <> IO.ANSI.reset
|
||||
IO.puts IO.ANSI.reset
|
||||
end
|
||||
|
||||
defp write_with_wrap([], _available, _indent, _first) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_with_wrap(words, available, indent, first) do
|
||||
{words, rest} = take_words(words, available, [])
|
||||
IO.puts (if first, do: "", else: indent) <> Enum.join(words, " ")
|
||||
write_with_wrap(rest, available, indent, false)
|
||||
end
|
||||
|
||||
defp take_words([word|words], available, acc) do
|
||||
available = available - length_without_escape(word, 0)
|
||||
|
||||
cond do
|
||||
# It fits, take one for space and continue decreasing
|
||||
available > 0 ->
|
||||
take_words(words, available - 1, [word|acc])
|
||||
|
||||
# No space but we got no words
|
||||
acc == [] ->
|
||||
{[word], words}
|
||||
|
||||
# Otherwise
|
||||
true ->
|
||||
{Enum.reverse(acc), [word|words]}
|
||||
end
|
||||
end
|
||||
|
||||
defp take_words([], _available, acc) do
|
||||
{Enum.reverse(acc), []}
|
||||
end
|
||||
|
||||
defp length_without_escape(<< ?\e, ?[, _, _, ?m, rest :: binary >>, count) do
|
||||
length_without_escape(rest, count)
|
||||
end
|
||||
|
||||
defp length_without_escape(<< ?\e, ?[, _, ?m, rest :: binary >>, count) do
|
||||
length_without_escape(rest, count)
|
||||
end
|
||||
|
||||
defp length_without_escape(rest, count) do
|
||||
case String.next_grapheme(rest) do
|
||||
{_, rest} -> length_without_escape(rest, count + 1)
|
||||
nil -> count
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_links(text) do
|
||||
text
|
||||
|> remove_square_brackets_in_link
|
||||
|> escape_underlines_in_link
|
||||
end
|
||||
|
||||
defp escape_underlines_in_link(text) do
|
||||
case Regex.match?(~r{.*(https?\S*)}, text) do
|
||||
true ->
|
||||
Regex.replace(~r{_}, text, "\\\\_")
|
||||
_ ->
|
||||
text
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_square_brackets_in_link(text) do
|
||||
Regex.replace(~r{\[(.*?)\]\((.*?)\)}, text, "\\1 (\\2)")
|
||||
end
|
||||
|
||||
# Single inline quotes.
|
||||
@single [?`, ?_, ?*]
|
||||
|
||||
# ` does not require space in between
|
||||
@spaced [?_, ?*]
|
||||
|
||||
# Clauses for handling spaces
|
||||
defp handle_inline(<<?*, ?*, ?\s, rest :: binary>>, nil, buffer, acc, options) do
|
||||
handle_inline(rest, nil, [?\s, ?*, ?*|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, ?\s, rest :: binary>>, nil, buffer, acc, options) when mark in @spaced do
|
||||
handle_inline(rest, nil, [?\s, mark|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?\s, ?*, ?*, rest :: binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?*, ?*, ?\s|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?\s, mark, rest :: binary>>, limit, buffer, acc, options) when mark in @spaced do
|
||||
handle_inline(rest, limit, [mark, ?\s|buffer], acc, options)
|
||||
end
|
||||
|
||||
# Clauses for handling escape
|
||||
defp handle_inline(<<?\\, ?\\, rest :: binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?\\|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?\\, ?*, ?*, rest :: binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?*, ?*|buffer], acc, options)
|
||||
end
|
||||
|
||||
# A escape is not valid inside `
|
||||
defp handle_inline(<<?\\, mark, rest :: binary>>, limit, buffer, acc, options)
|
||||
when mark in [?_, ?*, ?`] and not(mark == limit and mark == ?`) do
|
||||
handle_inline(rest, limit, [mark|buffer], acc, options)
|
||||
end
|
||||
|
||||
# Inline start
|
||||
defp handle_inline(<<?*, ?*, rest :: binary>>, nil, buffer, acc, options) when rest != "" do
|
||||
handle_inline(rest, ?d, ["**"], [Enum.reverse(buffer)|acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, rest :: binary>>, nil, buffer, acc, options) when rest != "" and mark in @single do
|
||||
handle_inline(rest, mark, [<<mark>>], [Enum.reverse(buffer)|acc], options)
|
||||
end
|
||||
|
||||
# Inline end
|
||||
defp handle_inline(<<?*, ?*, rest :: binary>>, ?d, buffer, acc, options) do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options)|acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, rest :: binary>>, mark, buffer, acc, options) when mark in @single do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options)|acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<char, rest :: binary>>, mark, buffer, acc, options) do
|
||||
handle_inline(rest, mark, [char|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<>>, _mark, buffer, acc, _options) do
|
||||
iodata_to_binary Enum.reverse([Enum.reverse(buffer)|acc])
|
||||
end
|
||||
|
||||
defp inline_buffer(buffer, options) do
|
||||
[h|t] = Enum.reverse([IO.ANSI.reset|buffer])
|
||||
[color_for(h, options)|t]
|
||||
end
|
||||
|
||||
defp color_for("`", colors), do: color(:doc_inline_code, colors)
|
||||
defp color_for("_", colors), do: color(:doc_underline, colors)
|
||||
defp color_for("*", colors), do: color(:doc_bold, colors)
|
||||
defp color_for("**", colors), do: color(:doc_bold, colors)
|
||||
|
||||
defp color(style, colors) do
|
||||
color = colors[style]
|
||||
enabled = colors[:enabled]
|
||||
IO.ANSI.escape_fragment("%{#{color}}", enabled)
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,391 +0,0 @@
|
||||
defmodule Kernel.CLI do
|
||||
@moduledoc false
|
||||
|
||||
@blank_config %{commands: [], output: ".", compile: [],
|
||||
halt: true, compiler_options: [], errors: [],
|
||||
verbose_compile: false}
|
||||
|
||||
@doc """
|
||||
This is the API invoked by Elixir boot process.
|
||||
"""
|
||||
def main(argv) do
|
||||
argv = for arg <- argv, do: String.from_char_data!(arg)
|
||||
|
||||
{config, argv} = process_argv(argv, @blank_config)
|
||||
System.argv(argv)
|
||||
|
||||
run fn ->
|
||||
command_results = Enum.map(Enum.reverse(config.commands), &process_command(&1, config))
|
||||
command_errors = for {:error, msg} <- command_results, do: msg
|
||||
errors = Enum.reverse(config.errors) ++ command_errors
|
||||
|
||||
if errors != [] do
|
||||
Enum.each(errors, &IO.puts(:stderr, &1))
|
||||
System.halt(1)
|
||||
end
|
||||
end, config.halt
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs the given function by catching any failure
|
||||
and printing them to stdout. `at_exit` hooks are
|
||||
also invoked before exiting.
|
||||
|
||||
This function is used by Elixir's CLI and also
|
||||
by escripts generated by Elixir.
|
||||
"""
|
||||
def run(fun, halt \\ true) do
|
||||
try do
|
||||
fun.()
|
||||
if halt do
|
||||
at_exit(0)
|
||||
System.halt(0)
|
||||
end
|
||||
catch
|
||||
:exit, reason when is_integer(reason) ->
|
||||
at_exit(reason)
|
||||
System.halt(reason)
|
||||
:exit, :normal ->
|
||||
at_exit(0)
|
||||
System.halt(0)
|
||||
kind, reason ->
|
||||
at_exit(1)
|
||||
print_error(kind, reason, System.stacktrace)
|
||||
System.halt(1)
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp at_exit(status) do
|
||||
hooks = :elixir_code_server.call(:flush_at_exit)
|
||||
|
||||
for hook <- hooks do
|
||||
try do
|
||||
hook.(status)
|
||||
catch
|
||||
kind, reason ->
|
||||
print_error(kind, reason, System.stacktrace)
|
||||
end
|
||||
end
|
||||
|
||||
# If an at_exit callback adds a
|
||||
# new hook we need to invoke it.
|
||||
unless hooks == [], do: at_exit(status)
|
||||
end
|
||||
|
||||
defp shared_option?(list, config, callback) do
|
||||
case process_shared(list, config) do
|
||||
{[h|hs], _} when h == hd(list) ->
|
||||
new_config = %{config | errors: ["#{h} : Unknown option" | config.errors]}
|
||||
callback.(hs, new_config)
|
||||
{new_list, new_config} ->
|
||||
callback.(new_list, new_config)
|
||||
end
|
||||
end
|
||||
|
||||
defp print_error(kind, reason, trace) do
|
||||
IO.puts :stderr, Exception.format(kind, reason, prune_stacktrace(trace))
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir_compiler, :elixir_module, :elixir_translator, :elixir_expand]
|
||||
|
||||
defp prune_stacktrace([{mod, _, _, _}|t]) when mod in @elixir_internals do
|
||||
prune_stacktrace(t)
|
||||
end
|
||||
|
||||
defp prune_stacktrace([{__MODULE__, :wrapper, 1, _}|_]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp prune_stacktrace([h|t]) do
|
||||
[h|prune_stacktrace(t)]
|
||||
end
|
||||
|
||||
defp prune_stacktrace([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
# Process shared options
|
||||
|
||||
defp process_shared([opt|_t], _config) when opt in ["-v", "--version"] do
|
||||
IO.puts "Elixir #{System.version}"
|
||||
System.halt 0
|
||||
end
|
||||
|
||||
defp process_shared(["-pa", h|t], config) do
|
||||
Enum.each Path.wildcard(Path.expand(h)), &Code.prepend_path(&1)
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(["-pz", h|t], config) do
|
||||
Enum.each Path.wildcard(Path.expand(h)), &Code.append_path(&1)
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(["--app", h|t], config) do
|
||||
process_shared t, %{config | commands: &[{:app, h}|&1]}
|
||||
end
|
||||
|
||||
defp process_shared(["--no-halt"|t], config) do
|
||||
process_shared t, %{config | halt: false}
|
||||
end
|
||||
|
||||
defp process_shared(["-e", h|t], config) do
|
||||
process_shared t, %{config | commands: [{:eval, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp process_shared(["-r", h|t], config) do
|
||||
process_shared t, %{config | commands: [{:require, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp process_shared(["-pr", h|t], config) do
|
||||
process_shared t, %{config | commands: [{:parallel_require, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp process_shared([erl, _|t], config) when erl in ["--erl", "--sname", "--name", "--cookie"] do
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared([erl|t], config) when erl in ["--detached", "--hidden", "--gen-debug"] do
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(list, config) do
|
||||
{list, config}
|
||||
end
|
||||
|
||||
# Process init options
|
||||
|
||||
defp process_argv(["--"|t], config) do
|
||||
{config, t}
|
||||
end
|
||||
|
||||
defp process_argv(["+elixirc"|t], config) do
|
||||
process_compiler t, config
|
||||
end
|
||||
|
||||
defp process_argv(["+iex"|t], config) do
|
||||
process_iex t, config
|
||||
end
|
||||
|
||||
defp process_argv(["-S", h|t], config) do
|
||||
{%{config | commands: [{:script, h} | config.commands]}, t}
|
||||
end
|
||||
|
||||
defp process_argv([h|t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
shared_option? list, config, &process_argv(&1, &2)
|
||||
_ ->
|
||||
{%{config | commands: [{:file, h} | config.commands]}, t}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_argv([], config) do
|
||||
{config, []}
|
||||
end
|
||||
|
||||
# Process compiler options
|
||||
|
||||
defp process_compiler(["--"|t], config) do
|
||||
{config, t}
|
||||
end
|
||||
|
||||
defp process_compiler(["-o", h|t], config) do
|
||||
process_compiler t, %{config | output: h}
|
||||
end
|
||||
|
||||
defp process_compiler(["--no-docs"|t], config) do
|
||||
process_compiler t, %{config | compiler_options: [{:docs, false} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp process_compiler(["--no-debug-info"|t], config) do
|
||||
process_compiler t, %{config | compiler_options: [{:debug_info, false} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp process_compiler(["--ignore-module-conflict"|t], config) do
|
||||
process_compiler t, %{config | compiler_options: [{:ignore_module_conflict, true} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp process_compiler(["--warnings-as-errors"|t], config) do
|
||||
process_compiler t, %{config | compiler_options: [{:warnings_as_errors, true} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp process_compiler(["--verbose"|t], config) do
|
||||
process_compiler t, %{config | verbose_compile: true}
|
||||
end
|
||||
|
||||
defp process_compiler([h|t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
shared_option? list, config, &process_compiler(&1, &2)
|
||||
_ ->
|
||||
pattern = if :filelib.is_dir(h), do: "#{h}/**/*.ex", else: h
|
||||
process_compiler t, %{config | compile: [pattern | config.compile]}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_compiler([], config) do
|
||||
{%{config | commands: [{:compile, config.compile}|config.commands]}, []}
|
||||
end
|
||||
|
||||
# Process iex options
|
||||
|
||||
defp process_iex(["--"|t], config) do
|
||||
{config, t}
|
||||
end
|
||||
|
||||
# This clause is here so that Kernel.CLI does not error out with "unknown
|
||||
# option"
|
||||
defp process_iex(["--dot-iex", _|t], config) do
|
||||
process_iex t, config
|
||||
end
|
||||
|
||||
defp process_iex([opt, _|t], config) when opt in ["--remsh"] do
|
||||
process_iex t, config
|
||||
end
|
||||
|
||||
defp process_iex(["-S", h|t], config) do
|
||||
{%{config | commands: [{:script, h} | config.commands]}, t}
|
||||
end
|
||||
|
||||
defp process_iex([h|t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
shared_option? list, config, &process_iex(&1, &2)
|
||||
_ ->
|
||||
{%{config | commands: [{:file, h} | config.commands]}, t}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_iex([], config) do
|
||||
{config, []}
|
||||
end
|
||||
|
||||
# Process commands
|
||||
|
||||
defp process_command({:cookie, h}, _config) do
|
||||
if Node.alive? do
|
||||
wrapper fn -> Node.set_cookie(binary_to_atom(h)) end
|
||||
else
|
||||
{:error, "--cookie : Cannot set cookie if the node is not alive (set --name or --sname)"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:eval, expr}, _config) when is_binary(expr) do
|
||||
wrapper fn -> Code.eval_string(expr, []) end
|
||||
end
|
||||
|
||||
defp process_command({:app, app}, _config) when is_binary(app) do
|
||||
case Application.ensure_all_started(binary_to_atom(app)) do
|
||||
{:error, {app, reason}} ->
|
||||
{:error, "--app : Could not start application #{app}: " <>
|
||||
Application.format_error(reason)}
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:script, file}, _config) when is_binary(file) do
|
||||
if exec = find_elixir_executable(file) do
|
||||
wrapper fn -> Code.require_file(exec) end
|
||||
else
|
||||
{:error, "-S : Could not find executable #{file}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:file, file}, _config) when is_binary(file) do
|
||||
if :filelib.is_regular(file) do
|
||||
wrapper fn -> Code.require_file(file) end
|
||||
else
|
||||
{:error, "No file named #{file}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:require, pattern}, _config) when is_binary(pattern) do
|
||||
files = Path.wildcard(pattern)
|
||||
files = Enum.uniq(files)
|
||||
files = Enum.filter files, &:filelib.is_regular(&1)
|
||||
|
||||
if files != [] do
|
||||
wrapper fn -> Enum.map files, &Code.require_file(&1) end
|
||||
else
|
||||
{:error, "-r : No files matched pattern #{pattern}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:parallel_require, pattern}, _config) when is_binary(pattern) do
|
||||
files = Path.wildcard(pattern)
|
||||
files = Enum.uniq(files)
|
||||
files = Enum.filter files, &:filelib.is_regular(&1)
|
||||
|
||||
if files != [] do
|
||||
wrapper fn -> Kernel.ParallelRequire.files(files) end
|
||||
else
|
||||
{:error, "-pr : No files matched pattern #{pattern}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:compile, patterns}, config) do
|
||||
:filelib.ensure_dir(:filename.join(config.output, "."))
|
||||
|
||||
case match_regular_files(patterns) do
|
||||
{:ok, []} ->
|
||||
{:error, "No files matched provided patterns"}
|
||||
{:ok, files} ->
|
||||
wrapper fn ->
|
||||
Code.compiler_options(config.compiler_options)
|
||||
Kernel.ParallelCompiler.files_to_path(files, config.output,
|
||||
each_file: fn file -> if config.verbose_compile do IO.puts "Compiled #{file}" end end)
|
||||
end
|
||||
{:missing, missing} ->
|
||||
{:error, "No files matched pattern(s) #{Enum.join(missing, ",")}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp match_regular_files(patterns) do
|
||||
matched_files = Enum.map patterns, fn(pattern) ->
|
||||
case Path.wildcard(pattern) do
|
||||
[] -> {:missing, pattern}
|
||||
files -> {:ok, files}
|
||||
end
|
||||
end
|
||||
|
||||
files = Enum.filter_map matched_files,
|
||||
fn(match) -> elem(match, 0) == :ok end,
|
||||
&elem(&1, 1)
|
||||
|
||||
missing_patterns = Enum.filter_map matched_files,
|
||||
fn(match) -> elem(match, 0) == :missing end,
|
||||
&elem(&1, 1)
|
||||
|
||||
if missing_patterns == [] do
|
||||
files = Enum.uniq(Enum.concat(files))
|
||||
files = Enum.filter files, &:filelib.is_regular(&1)
|
||||
{:ok, files}
|
||||
else
|
||||
{:missing, Enum.uniq(missing_patterns)}
|
||||
end
|
||||
end
|
||||
|
||||
defp wrapper(fun) do
|
||||
fun.()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp find_elixir_executable(file) do
|
||||
if exec = System.find_executable(file) do
|
||||
# If we are on Windows, the executable is going to be
|
||||
# a .bat file that must be in the same directory as
|
||||
# the actual Elixir executable.
|
||||
case :os.type() do
|
||||
{:win32, _} ->
|
||||
exec = Path.rootname(exec)
|
||||
if File.regular?(exec), do: exec
|
||||
_ ->
|
||||
exec
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# Implement error_handler pattern for Erlang
|
||||
# which is integrated with Kernel.ParallelCompiler
|
||||
defmodule Kernel.ErrorHandler do
|
||||
@moduledoc false
|
||||
|
||||
def undefined_function(module, fun, args) do
|
||||
ensure_loaded(module)
|
||||
:error_handler.undefined_function(module, fun, args)
|
||||
end
|
||||
|
||||
def undefined_lambda(module, fun, args) do
|
||||
ensure_loaded(module)
|
||||
:error_handler.undefined_lambda(module, fun, args)
|
||||
end
|
||||
|
||||
def release() do
|
||||
# On release, no further allow elixir_ensure_compiled
|
||||
# directives and revert to the original error handler.
|
||||
# Note we should not delete the elixir_compiler_pid though,
|
||||
# as we still want to send notifications to the compiler.
|
||||
:erlang.erase(:elixir_ensure_compiled)
|
||||
:erlang.process_flag(:error_handler, :error_handler)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_loaded(module) do
|
||||
case Code.ensure_loaded(module) do
|
||||
{:module, _} -> []
|
||||
{:error, _} ->
|
||||
parent = :erlang.get(:elixir_compiler_pid)
|
||||
ref = :erlang.make_ref
|
||||
send parent, {:waiting, module, self(), ref, module}
|
||||
:erlang.garbage_collect(self)
|
||||
receive do
|
||||
{^ref, :ready} -> :ok
|
||||
{^ref, :release} -> release()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,175 +0,0 @@
|
||||
# This is a module Elixir responsible for tracking
|
||||
# the usage of aliases, imports and requires in the Elixir scope.
|
||||
#
|
||||
# The implementation simply stores dispatch information in an
|
||||
# ETS table and then consults this table once compilation is done.
|
||||
#
|
||||
# Note that since this is required for bootstrap, we can't use
|
||||
# any of the `GenServer.Behaviour` conveniences.
|
||||
defmodule Kernel.LexicalTracker do
|
||||
@timeout 30_000
|
||||
@behaviour :gen_server
|
||||
|
||||
@import 2
|
||||
@alias 3
|
||||
|
||||
@doc """
|
||||
Returns all remotes linked to in this lexical scope.
|
||||
"""
|
||||
def remotes(arg) do
|
||||
# If the module is compiled from a function, its lexical
|
||||
# scope may be long gone, so it has no associated PID.
|
||||
if pid = to_pid(arg) do
|
||||
ets = :gen_server.call(pid, :ets, @timeout)
|
||||
:ets.match(ets, {:"$1", :_, :_}) |> List.flatten
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp to_pid(pid) when is_pid(pid), do: pid
|
||||
defp to_pid(mod) when is_atom(mod) do
|
||||
table = :elixir_module.data_table(mod)
|
||||
[{_, val}] = :ets.lookup(table, :__lexical_tracker)
|
||||
val
|
||||
end
|
||||
|
||||
# Internal API
|
||||
|
||||
# Starts the tracker and returns its pid.
|
||||
@doc false
|
||||
def start_link do
|
||||
{:ok, pid} = :gen_server.start_link(__MODULE__, [], [])
|
||||
pid
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stop(pid) do
|
||||
:gen_server.cast(pid, :stop)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, line, warn) do
|
||||
:gen_server.cast(pid, {:add_import, module, line, warn})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_alias(pid, module, line, warn) do
|
||||
:gen_server.cast(pid, {:add_alias, module, line, warn})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_dispatch(pid, module) do
|
||||
:gen_server.cast(pid, {:remote_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def import_dispatch(pid, module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def alias_dispatch(pid, module) do
|
||||
:gen_server.cast(pid, {:alias_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_imports(pid) do
|
||||
unused(pid, @import)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_aliases(pid) do
|
||||
unused(pid, @alias)
|
||||
end
|
||||
|
||||
defp unused(pid, pos) do
|
||||
ets = :gen_server.call(pid, :ets, @timeout)
|
||||
:ets.foldl(fn
|
||||
{module, _, _} = tuple, acc when is_integer(:erlang.element(pos, tuple)) ->
|
||||
[{module, :erlang.element(pos, tuple)}|acc]
|
||||
_, acc ->
|
||||
acc
|
||||
end, [], ets) |> Enum.sort
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
|
||||
def init([]) do
|
||||
{:ok, :ets.new(:lexical, [:protected])}
|
||||
end
|
||||
|
||||
def handle_call(:ets, _from, d) do
|
||||
{:reply, d, d}
|
||||
end
|
||||
|
||||
def handle_call(request, _from, d) do
|
||||
{:stop, {:bad_call, request}, d}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_dispatch, module}, d) do
|
||||
add_module(d, module)
|
||||
{:noreply, d}
|
||||
end
|
||||
|
||||
def handle_cast({:import_dispatch, module}, d) do
|
||||
add_dispatch(d, module, @import)
|
||||
{:noreply, d}
|
||||
end
|
||||
|
||||
def handle_cast({:alias_dispatch, module}, d) do
|
||||
add_dispatch(d, module, @alias)
|
||||
{:noreply, d}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, module, line, warn}, d) do
|
||||
add_directive(d, module, line, warn, @import)
|
||||
{:noreply, d}
|
||||
end
|
||||
|
||||
def handle_cast({:add_alias, module, line, warn}, d) do
|
||||
add_directive(d, module, line, warn, @alias)
|
||||
{:noreply, d}
|
||||
end
|
||||
|
||||
def handle_cast(:stop, d) do
|
||||
{:stop, :normal, d}
|
||||
end
|
||||
|
||||
def handle_cast(msg, d) do
|
||||
{:stop, {:bad_cast, msg}, d}
|
||||
end
|
||||
|
||||
def handle_info(_msg, d) do
|
||||
{:noreply, d}
|
||||
end
|
||||
|
||||
def terminate(_reason, _d) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def code_change(_old, d, _extra) do
|
||||
{:ok, d}
|
||||
end
|
||||
|
||||
# Callbacks helpers
|
||||
|
||||
# In the table we keep imports and aliases.
|
||||
# If the value is false, it was not imported/aliased
|
||||
# If the value is true, it was imported/aliased
|
||||
# If the value is a line, it was imported/aliased and has a pending warning
|
||||
defp add_module(d, module) do
|
||||
:ets.insert_new(d, {module, false, false})
|
||||
end
|
||||
|
||||
defp add_dispatch(d, module, pos) do
|
||||
:ets.update_element(d, module, {pos, true})
|
||||
end
|
||||
|
||||
defp add_directive(d, module, line, warn, pos) do
|
||||
add_module(d, module)
|
||||
marker = if warn, do: line, else: true
|
||||
:ets.update_element(d, module, {pos, marker})
|
||||
end
|
||||
end
|
||||
@@ -1,247 +0,0 @@
|
||||
defmodule Kernel.ParallelCompiler do
|
||||
@moduledoc """
|
||||
A module responsible for compiling files in parallel.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Compiles the given files.
|
||||
|
||||
Those files are compiled in parallel and can automatically
|
||||
detect dependencies between them. Once a dependency is found,
|
||||
the current file stops being compiled until the dependency is
|
||||
resolved.
|
||||
|
||||
If there is an error during compilation or if `warnings_as_errors`
|
||||
is set to `true` and there is a warning, this function will fail
|
||||
with an exception.
|
||||
|
||||
This function receives a set of callbacks as options:
|
||||
|
||||
* `:each_file` - for each file compiled, invokes the callback passing the file
|
||||
* `:each_module` - for each module compiled, invokes the callback
|
||||
passing the file, module and the module bytecode
|
||||
|
||||
The compiler doesn't care about the return values of the callbacks.
|
||||
Returns the modules generated by each compiled file.
|
||||
"""
|
||||
def files(files, callbacks \\ [])
|
||||
|
||||
def files(files, callbacks) when is_list(callbacks) do
|
||||
spawn_compilers(files, nil, callbacks)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given files to the given path.
|
||||
Read `files/2` for more information.
|
||||
"""
|
||||
def files_to_path(files, path, callbacks \\ [])
|
||||
|
||||
def files_to_path(files, path, callbacks) when is_binary(path) and is_list(callbacks) do
|
||||
spawn_compilers(files, path, callbacks)
|
||||
end
|
||||
|
||||
defp spawn_compilers(files, path, callbacks) do
|
||||
Code.ensure_loaded(Kernel.ErrorHandler)
|
||||
compiler_pid = self()
|
||||
:elixir_code_server.cast({:reset_warnings, compiler_pid})
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
|
||||
result = spawn_compilers(files, files, path, callbacks, [], [], schedulers, [])
|
||||
|
||||
# In case --warning-as-errors is enabled and there was a warning,
|
||||
# compilation status will be set to error and we fail with CompileError
|
||||
case :elixir_code_server.call({:compilation_status, compiler_pid}) do
|
||||
:ok -> result
|
||||
:error -> exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
# We already have 4 currently running, don't spawn new ones
|
||||
defp spawn_compilers(entries, original, output, callbacks, waiting, queued, schedulers, result) when
|
||||
length(queued) - length(waiting) >= schedulers do
|
||||
wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# Release waiting processes
|
||||
defp spawn_compilers([h|t], original, output, callbacks, waiting, queued, schedulers, result) when is_pid(h) do
|
||||
{_kind, ^h, ref, _module} = List.keyfind(waiting, h, 1)
|
||||
send h, {ref, :ready}
|
||||
waiting = List.keydelete(waiting, h, 1)
|
||||
spawn_compilers(t, original, output, callbacks, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# Spawn a compiler for each file in the list until we reach the limit
|
||||
defp spawn_compilers([h|t], original, output, callbacks, waiting, queued, schedulers, result) do
|
||||
parent = self()
|
||||
|
||||
{pid, ref} =
|
||||
:erlang.spawn_monitor fn ->
|
||||
# Notify Code.ensure_compiled/2 that we should
|
||||
# attempt to compile the module by doing a dispatch.
|
||||
:erlang.put(:elixir_ensure_compiled, true)
|
||||
|
||||
# Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
|
||||
:erlang.put(:elixir_compiler_pid, parent)
|
||||
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
|
||||
|
||||
exit(try do
|
||||
if output do
|
||||
:elixir_compiler.file_to_path(h, output)
|
||||
else
|
||||
:elixir_compiler.file(h)
|
||||
end
|
||||
{:compiled, h}
|
||||
catch
|
||||
kind, reason ->
|
||||
{:failure, kind, reason, System.stacktrace}
|
||||
end)
|
||||
end
|
||||
|
||||
spawn_compilers(t, original, output, callbacks, waiting,
|
||||
[{pid, ref, h}|queued], schedulers, result)
|
||||
end
|
||||
|
||||
# No more files, nothing waiting, queue is empty, we are done
|
||||
defp spawn_compilers([], _original, _output, _callbacks, [], [], _schedulers, result) do
|
||||
for {:module, mod} <- result, do: mod
|
||||
end
|
||||
|
||||
# Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
|
||||
defp spawn_compilers([], original, output, callbacks, waiting, queued, schedulers, result) when length(waiting) == length(queued) do
|
||||
Enum.each queued, fn {child, _, _} ->
|
||||
{_kind, ^child, ref, _module} = List.keyfind(waiting, child, 1)
|
||||
send child, {ref, :release}
|
||||
end
|
||||
wait_for_messages([], original, output, callbacks, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# No more files, but queue and waiting are not full or do not match
|
||||
defp spawn_compilers([], original, output, callbacks, waiting, queued, schedulers, result) do
|
||||
wait_for_messages([], original, output, callbacks, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# Wait for messages from child processes
|
||||
defp wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result) do
|
||||
receive do
|
||||
{:struct_available, module} ->
|
||||
available = for {:struct, pid, _, waiting_module} <- waiting,
|
||||
module == waiting_module,
|
||||
not pid in entries,
|
||||
do: pid
|
||||
|
||||
spawn_compilers(available ++ entries, original, output, callbacks,
|
||||
waiting, queued, schedulers, [{:struct, module}|result])
|
||||
|
||||
{:module_available, child, ref, file, module, binary} ->
|
||||
if callback = Keyword.get(callbacks, :each_module) do
|
||||
callback.(file, module, binary)
|
||||
end
|
||||
|
||||
# Release the module loader which is waiting for an ack
|
||||
send child, {ref, :ack}
|
||||
|
||||
available = for {_kind, pid, _, waiting_module} <- waiting,
|
||||
module == waiting_module,
|
||||
not pid in entries,
|
||||
do: pid
|
||||
|
||||
spawn_compilers(available ++ entries, original, output, callbacks,
|
||||
waiting, queued, schedulers, [{:module, module}|result])
|
||||
|
||||
{:waiting, kind, child, ref, on} ->
|
||||
defined = fn {k, m} -> on == m and k in [kind, :module] end
|
||||
|
||||
# Oops, we already got it, do not put it on waiting.
|
||||
if :lists.any(defined, result) do
|
||||
send child, {ref, :ready}
|
||||
else
|
||||
waiting = [{kind, child, ref, on}|waiting]
|
||||
end
|
||||
|
||||
spawn_compilers(entries, original, output, callbacks, waiting, queued, schedulers, result)
|
||||
|
||||
{:DOWN, _down_ref, :process, down_pid, {:compiled, file}} ->
|
||||
if callback = Keyword.get(callbacks, :each_file) do
|
||||
callback.(file)
|
||||
end
|
||||
|
||||
# Sometimes we may have spurious entries in the waiting
|
||||
# list because someone invoked try/rescue UndefinedFunctionError
|
||||
new_entries = List.delete(entries, down_pid)
|
||||
new_queued = List.keydelete(queued, down_pid, 0)
|
||||
new_waiting = List.keydelete(waiting, down_pid, 1)
|
||||
spawn_compilers(new_entries, original, output, callbacks, new_waiting, new_queued, schedulers, result)
|
||||
|
||||
{:DOWN, down_ref, :process, _down_pid, reason} ->
|
||||
handle_failure(down_ref, reason, entries, waiting, queued)
|
||||
wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_failure(ref, reason, entries, waiting, queued) do
|
||||
if file = find_failure(ref, queued) do
|
||||
print_failure(file, reason)
|
||||
if all_missing?(entries, waiting, queued) do
|
||||
collect_failures(queued, length(queued) - 1)
|
||||
end
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
defp find_failure(ref, queued) do
|
||||
case List.keyfind(queued, ref, 1) do
|
||||
{_child, ^ref, file} -> file
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp print_failure(_file, {:compiled, _}) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp print_failure(file, {:failure, kind, reason, stacktrace}) do
|
||||
IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
|
||||
IO.puts Exception.format(kind, reason, prune_stacktrace(stacktrace))
|
||||
end
|
||||
|
||||
defp print_failure(file, reason) do
|
||||
IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
|
||||
IO.puts Exception.format(:exit, reason, [])
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir_compiler, :elixir_module, :elixir_translator, :elixir_expand]
|
||||
|
||||
defp prune_stacktrace([{mod, _, _, _}|t]) when mod in @elixir_internals do
|
||||
prune_stacktrace(t)
|
||||
end
|
||||
|
||||
defp prune_stacktrace([h|t]) do
|
||||
[h|prune_stacktrace(t)]
|
||||
end
|
||||
|
||||
defp prune_stacktrace([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp all_missing?(entries, waiting, queued) do
|
||||
entries == [] and waiting != [] and
|
||||
length(waiting) == length(queued)
|
||||
end
|
||||
|
||||
defp collect_failures(_queued, 0), do: :ok
|
||||
|
||||
defp collect_failures(queued, remaining) do
|
||||
receive do
|
||||
{:DOWN, down_ref, :process, _down_pid, reason} ->
|
||||
if file = find_failure(down_ref, queued) do
|
||||
print_failure(file, reason)
|
||||
collect_failures(queued, remaining - 1)
|
||||
else
|
||||
collect_failures(queued, remaining)
|
||||
end
|
||||
after
|
||||
# Give up if no failure appears in 5 seconds
|
||||
5000 -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,80 +0,0 @@
|
||||
defmodule Kernel.ParallelRequire do
|
||||
@moduledoc """
|
||||
A module responsible for requiring files in parallel.
|
||||
"""
|
||||
|
||||
defmacrop default_callback, do: quote(do: fn x -> x end)
|
||||
|
||||
@doc """
|
||||
Requires the given files.
|
||||
|
||||
A callback that is invoked every time a file is required
|
||||
can be optionally given as argument.
|
||||
|
||||
Returns the modules generated by each required file.
|
||||
"""
|
||||
def files(files, callback \\ default_callback) do
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
spawn_requires(files, [], callback, schedulers, [])
|
||||
end
|
||||
|
||||
defp spawn_requires([], [], _callback, _schedulers, result), do: result
|
||||
|
||||
defp spawn_requires([], waiting, callback, schedulers, result) do
|
||||
wait_for_messages([], waiting, callback, schedulers, result)
|
||||
end
|
||||
|
||||
defp spawn_requires(files, waiting, callback, schedulers, result) when length(waiting) >= schedulers do
|
||||
wait_for_messages(files, waiting, callback, schedulers, result)
|
||||
end
|
||||
|
||||
defp spawn_requires([h|t], waiting, callback, schedulers, result) do
|
||||
parent = self
|
||||
|
||||
compiler_pid = :erlang.get(:elixir_compiler_pid)
|
||||
ensure_compiled = :erlang.get(:elixir_ensure_compiled)
|
||||
{:error_handler, handler} = :erlang.process_info(parent, :error_handler)
|
||||
|
||||
{pid, ref} = :erlang.spawn_monitor fn ->
|
||||
if compiler_pid != :undefined do
|
||||
:erlang.put(:elixir_compiler_pid, compiler_pid)
|
||||
end
|
||||
|
||||
if ensure_compiled != :undefined do
|
||||
:erlang.put(:elixir_ensure_compiled, ensure_compiled)
|
||||
end
|
||||
|
||||
:erlang.process_flag(:error_handler, handler)
|
||||
|
||||
exit(try do
|
||||
new = Code.require_file(h) || []
|
||||
{:required, Enum.map(new, &elem(&1, 0)), h}
|
||||
catch
|
||||
kind, reason ->
|
||||
{:failure, kind, reason, System.stacktrace}
|
||||
end)
|
||||
end
|
||||
|
||||
spawn_requires(t, [{pid, ref}|waiting], callback, schedulers, result)
|
||||
end
|
||||
|
||||
defp wait_for_messages(files, waiting, callback, schedulers, result) do
|
||||
receive do
|
||||
{:DOWN, ref, :process, pid, status} ->
|
||||
tuple = {pid, ref}
|
||||
if tuple in waiting do
|
||||
case status do
|
||||
{:required, mods, file} ->
|
||||
callback.(file)
|
||||
result = mods ++ result
|
||||
waiting = List.delete(waiting, tuple)
|
||||
{:failure, kind, reason, stacktrace} ->
|
||||
:erlang.raise(kind, reason, stacktrace)
|
||||
other ->
|
||||
:erlang.raise(:exit, other, [])
|
||||
end
|
||||
end
|
||||
spawn_requires(files, waiting, callback, schedulers, result)
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,979 +0,0 @@
|
||||
defmodule Kernel.Typespec do
|
||||
@moduledoc """
|
||||
Provides macros and functions for working with typespecs.
|
||||
|
||||
Elixir comes with a notation for declaring types and specifications. Elixir is
|
||||
dynamically typed, as such typespecs are never used by the compiler to
|
||||
optimize or modify code. Still, using typespecs is useful as documentation and
|
||||
tools such as [Dialyzer](http://www.erlang.org/doc/man/dialyzer.html) can
|
||||
analyze the code with typespecs to find bugs.
|
||||
|
||||
The attributes `@type`, `@opaque`, `@typep`, `@spec` and `@callback` available
|
||||
in modules are handled by the equivalent macros defined by this module. See
|
||||
sub-sections "Defining a type" and "Defining a specification" below.
|
||||
|
||||
## Types and their syntax
|
||||
|
||||
The type syntax provided by Elixir is fairly similar to the one in
|
||||
[Erlang](http://www.erlang.org/doc/reference_manual/typespec.html).
|
||||
|
||||
Most of the built-in types provided in Erlang (for example, `pid()`) are
|
||||
expressed the same way: `pid()` or simply `pid`. Parametrized types are also
|
||||
supported (`list(integer)`) and so are remote types (`Enum.t`).
|
||||
|
||||
Integers and atom literals are allowed as types (ex. `1`, `:atom` or
|
||||
`false`). All other types are built of unions of predefined types. Certain
|
||||
shorthands are allowed, such as `[...]`, `<<>>` and `{...}`.
|
||||
|
||||
### Predefined types
|
||||
|
||||
Type :: any # the top type, the set of all terms
|
||||
| none # the bottom type, contains no terms
|
||||
| pid
|
||||
| port
|
||||
| reference
|
||||
| Atom
|
||||
| Bitstring
|
||||
| float
|
||||
| Fun
|
||||
| Integer
|
||||
| List
|
||||
| Tuple
|
||||
| Union
|
||||
| UserDefined # Described in section "Defining a type"
|
||||
|
||||
Atom :: atom
|
||||
| ElixirAtom # `:foo`, `:bar`, ...
|
||||
|
||||
Bitstring :: <<>>
|
||||
| << _ :: M >> # M is a positive integer
|
||||
| << _ :: _ * N >> # N is a positive integer
|
||||
| << _ :: M, _ :: _ * N >>
|
||||
|
||||
Fun :: (... -> any) # any function
|
||||
| (... -> Type) # any arity, returning Type
|
||||
| (() -> Type))
|
||||
| (TList -> Type)
|
||||
|
||||
Integer :: integer
|
||||
| ElixirInteger # ..., -1, 0, 1, ... 42 ...
|
||||
| ElixirInteger..ElixirInteger # an integer range
|
||||
|
||||
List :: list(Type) # proper list ([]-terminated)
|
||||
| improper_list(Type1, Type2) # Type1=contents, Type2=termination
|
||||
| maybe_improper_list(Type1, Type2) # Type1 and Type2 as above
|
||||
| nonempty_list(Type) # proper non-empty list
|
||||
| [] # empty list
|
||||
| [Type] # shorthand for list(Type)
|
||||
| [Type, ...] # shorthand for nonempty_list(Type)
|
||||
|
||||
Tuple :: tuple # a tuple of any size
|
||||
| {} # empty tuple
|
||||
| {TList}
|
||||
|
||||
TList :: Type
|
||||
| Type, TList
|
||||
|
||||
Union :: Type1 | Type2
|
||||
|
||||
### Bit strings
|
||||
|
||||
Bit string with a base size of 3:
|
||||
|
||||
<< _ :: 3 >>
|
||||
|
||||
Bit string with a unit size of 8:
|
||||
|
||||
<< _ :: _ * 8 >>
|
||||
|
||||
### Anonymous functions
|
||||
|
||||
Any anonymous function:
|
||||
|
||||
((...) -> any)
|
||||
(... -> any)
|
||||
|
||||
Anonymous function with arity of zero:
|
||||
|
||||
(() -> type)
|
||||
|
||||
Anonymous function with some arity:
|
||||
|
||||
((type, type) -> type)
|
||||
(type, type -> type)
|
||||
|
||||
## Built-in types
|
||||
|
||||
Built-in type | Defined as
|
||||
:-------------------- | :---------
|
||||
`term` | `any`
|
||||
`binary` | `<< _ :: _ * 8 >>`
|
||||
`bitstring` | `<< _ :: _ * 1 >>`
|
||||
`boolean` | `false` | `true`
|
||||
`byte` | `0..255`
|
||||
`char` | `0..0xffff`
|
||||
`number` | `integer` | `float`
|
||||
`list` | `[any]`
|
||||
`maybe_improper_list` | `maybe_improper_list(any, any)`
|
||||
`nonempty_list` | `nonempty_list(any)`
|
||||
`iodata` | `iolist` | `binary`
|
||||
`iolist` | `maybe_improper_list(byte` | `binary` | `iolist, binary` | `[])`
|
||||
`module` | `atom`
|
||||
`mfa` | `{atom, atom, arity}`
|
||||
`arity` | `0..255`
|
||||
`node` | `atom`
|
||||
`timeout` | `:infinity` | `non_neg_integer`
|
||||
`no_return` | `none`
|
||||
`fun` | `(... -> any)`
|
||||
|
||||
Some built-in types cannot be expressed with valid syntax according to the
|
||||
language defined above.
|
||||
|
||||
Built-in type | Can be interpreted as
|
||||
:---------------- | :--------------------
|
||||
`non_neg_integer` | `0..`
|
||||
`pos_integer` | `1..`
|
||||
`neg_integer` | `..-1`
|
||||
|
||||
Types defined in other modules are referred to as "remote types", they are
|
||||
referenced as `Module.type_name` (ex. `Enum.t` or `String.t`).
|
||||
|
||||
## Defining a type
|
||||
|
||||
@type type_name :: type
|
||||
@typep type_name :: type
|
||||
@opaque type_name :: type
|
||||
|
||||
A type defined with `@typep` is private. An opaque type, defined with
|
||||
`@opaque` is a type where the internal structure of the type will not be
|
||||
visible, but the type is still public.
|
||||
|
||||
Types can be parametrised by defining variables as parameters, these variables
|
||||
can then be used to define the type.
|
||||
|
||||
@type dict(key, value) :: [{key, value}]
|
||||
|
||||
Types can also be defined for records, see `defrecord/3`.
|
||||
|
||||
## Defining a specification
|
||||
|
||||
@spec function_name(type1, type2) :: return_type
|
||||
@callback function_name(type1, type2) :: return_type
|
||||
|
||||
Callbacks are used to define the callbacks functions of behaviours (see
|
||||
`Behaviour`).
|
||||
|
||||
Guards can be used to restrict type variables given as arguments to the
|
||||
function.
|
||||
|
||||
@spec function(arg) :: [arg] when arg: atom
|
||||
|
||||
Type variables with no restriction can also be defined.
|
||||
|
||||
@spec function(arg) :: [arg] when arg: var
|
||||
|
||||
Specifications can be overloaded just like ordinary functions.
|
||||
|
||||
@spec function(integer) :: atom
|
||||
@spec function(atom) :: integer
|
||||
|
||||
## Notes
|
||||
|
||||
Elixir discourages the use of type `string` as it might be confused with
|
||||
binaries which are referred to as "strings" in Elixir (as opposed to character
|
||||
lists). In order to use the type that is called `string` in Erlang, one has to
|
||||
use the `char_list` type which is a synonym for `string`. If you use `string`,
|
||||
you'll get a warning from the compiler.
|
||||
|
||||
If you want to refer to the "string" type (the one operated on by functions in
|
||||
the `String` module), use `String.t` type instead.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Defines a type.
|
||||
This macro is responsible for handling the attribute `@type`.
|
||||
|
||||
## Examples
|
||||
|
||||
@type my_type :: atom
|
||||
|
||||
"""
|
||||
defmacro deftype(type) do
|
||||
quote do
|
||||
Kernel.Typespec.deftype(:type, unquote(Macro.escape(type, unquote: true)), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines an opaque type.
|
||||
This macro is responsible for handling the attribute `@opaque`.
|
||||
|
||||
## Examples
|
||||
|
||||
@opaque my_type :: atom
|
||||
|
||||
"""
|
||||
defmacro defopaque(type) do
|
||||
quote do
|
||||
Kernel.Typespec.deftype(:opaque, unquote(Macro.escape(type, unquote: true)), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a private type.
|
||||
This macro is responsible for handling the attribute `@typep`.
|
||||
|
||||
## Examples
|
||||
|
||||
@typep my_type :: atom
|
||||
|
||||
"""
|
||||
defmacro deftypep(type) do
|
||||
quote do
|
||||
Kernel.Typespec.deftype(:typep, unquote(Macro.escape(type, unquote: true)), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a spec.
|
||||
This macro is responsible for handling the attribute `@spec`.
|
||||
|
||||
## Examples
|
||||
|
||||
@spec add(number, number) :: number
|
||||
|
||||
"""
|
||||
defmacro defspec(spec) do
|
||||
quote do
|
||||
Kernel.Typespec.defspec(:spec, unquote(Macro.escape(spec, unquote: true)), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a callback.
|
||||
This macro is responsible for handling the attribute `@callback`.
|
||||
|
||||
## Examples
|
||||
|
||||
@callback add(number, number) :: number
|
||||
|
||||
"""
|
||||
defmacro defcallback(spec) do
|
||||
quote do
|
||||
Kernel.Typespec.defspec(:callback, unquote(Macro.escape(spec, unquote: true)), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
@doc """
|
||||
Defines a `type`, `typep` or `opaque` by receiving Erlang's typespec.
|
||||
"""
|
||||
def define_type(caller, kind, {name, _, vars} = type) when kind in [:type, :typep, :opaque] do
|
||||
{kind, export} =
|
||||
case kind do
|
||||
:type -> {:type, true}
|
||||
:typep -> {:type, false}
|
||||
:opaque -> {:opaque, true}
|
||||
end
|
||||
|
||||
module = caller.module
|
||||
arity = length(vars)
|
||||
|
||||
Module.compile_typespec module, kind, type
|
||||
|
||||
if export do
|
||||
Module.compile_typespec(module, :export_type, [{name, arity}])
|
||||
end
|
||||
|
||||
define_doc(caller, kind, name, arity, export)
|
||||
type
|
||||
end
|
||||
|
||||
defp define_doc(caller, kind, name, arity, export) do
|
||||
module = caller.module
|
||||
doc = Module.get_attribute(module, :typedoc)
|
||||
|
||||
if doc do
|
||||
if export do
|
||||
Module.add_doc(module, caller.line, kind, {name, arity}, doc)
|
||||
else
|
||||
:elixir_errors.warn caller.line, caller.file, "type #{name}/#{arity} is private, " <>
|
||||
"@typedoc's are always discarded for private types\n"
|
||||
end
|
||||
end
|
||||
|
||||
Module.delete_attribute(module, :typedoc)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a `spec` by receiving Erlang's typespec.
|
||||
"""
|
||||
def define_spec(module, tuple, definition) do
|
||||
Module.compile_typespec module, :spec, {tuple, definition}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a `callback` by receiving Erlang's typespec.
|
||||
"""
|
||||
def define_callback(module, tuple, definition) do
|
||||
Module.compile_typespec module, :callback, {tuple, definition}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if the current module defines a given type
|
||||
(private, opaque or not). This function is only available
|
||||
for modules being compiled.
|
||||
"""
|
||||
def defines_type?(module, name, arity) do
|
||||
finder = &match?({^name, _, vars} when length(vars) == arity, &1)
|
||||
Enum.any?(Module.get_attribute(module, :type), finder) or
|
||||
Enum.any?(Module.get_attribute(module, :opaque), finder)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if the current module defines a given spec.
|
||||
This function is only available for modules being compiled.
|
||||
"""
|
||||
def defines_spec?(module, name, arity) do
|
||||
tuple = {name, arity}
|
||||
Enum.any?(Module.get_attribute(module, :spec), &match?(^tuple, &1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if the current module defines a callback.
|
||||
This function is only available for modules being compiled.
|
||||
"""
|
||||
def defines_callback?(module, name, arity) do
|
||||
tuple = {name, arity}
|
||||
Enum.any?(Module.get_attribute(module, :callback), &match?(^tuple, &1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a spec clause back to Elixir AST.
|
||||
"""
|
||||
def spec_to_ast(name, {:type, line, :fun, [{:type, _, :product, args}, result]}) do
|
||||
meta = [line: line]
|
||||
body = {name, meta, Enum.map(args, &typespec_to_ast/1)}
|
||||
|
||||
vars = args ++ [result]
|
||||
|> Enum.flat_map(&collect_vars/1)
|
||||
|> Enum.uniq
|
||||
|> Enum.map(&{&1, {:var, meta, nil}})
|
||||
|
||||
spec = {:::, meta, [body, typespec_to_ast(result)]}
|
||||
|
||||
if vars == [] do
|
||||
spec
|
||||
else
|
||||
{:when, meta, [spec, vars]}
|
||||
end
|
||||
end
|
||||
|
||||
def spec_to_ast(name, {:type, line, :fun, []}) do
|
||||
{:::, [line: line], [{name, [line: line], []}, quote(do: term)]}
|
||||
end
|
||||
|
||||
def spec_to_ast(name, {:type, line, :bounded_fun, [{:type, _, :fun, [{:type, _, :product, args}, result]}, constraints]}) do
|
||||
guards =
|
||||
for {:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, type]]} <- constraints do
|
||||
{var, typespec_to_ast(type)}
|
||||
end
|
||||
|
||||
meta = [line: line]
|
||||
|
||||
vars = args ++ [result]
|
||||
|> Enum.flat_map(&collect_vars/1)
|
||||
|> Enum.uniq
|
||||
|> Kernel.--(Keyword.keys(guards))
|
||||
|> Enum.map(&{&1, {:var, meta, nil}})
|
||||
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
|
||||
{:when, meta, [
|
||||
{:::, meta, [{name, [line: line], args}, typespec_to_ast(result)]},
|
||||
guards ++ vars
|
||||
]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a type clause back to Elixir AST.
|
||||
"""
|
||||
def type_to_ast({{:record, record}, fields, args}) when is_atom(record) do
|
||||
fields = for field <- fields, do: typespec_to_ast(field)
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
type = {:{}, [], [record|fields]}
|
||||
quote do: unquote(record)(unquote_splicing(args)) :: unquote(type)
|
||||
end
|
||||
|
||||
def type_to_ast({name, type, args}) do
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
quote do: unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_ast(type))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all type docs available from the module's beam code.
|
||||
|
||||
The result is returned as a list of tuples where the first element is the pair of type
|
||||
name and arity and the second element is the documentation.
|
||||
|
||||
The module must have a corresponding beam file which can be
|
||||
located by the runtime system.
|
||||
"""
|
||||
@spec beam_typedocs(module | binary) :: [tuple] | nil
|
||||
def beam_typedocs(module) when is_atom(module) or is_binary(module) do
|
||||
case abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
type_docs = for {:attribute, _, :typedoc, tup} <- abstract_code, do: tup
|
||||
List.flatten(type_docs)
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all types available from the module's beam code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is the type (`:typep`, `:type` and `:opaque`).
|
||||
|
||||
The module must have a corresponding beam file which can be
|
||||
located by the runtime system.
|
||||
"""
|
||||
@spec beam_types(module | binary) :: [tuple] | nil
|
||||
def beam_types(module) when is_atom(module) or is_binary(module) do
|
||||
case abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
exported_types = for {:attribute, _, :export_type, types} <- abstract_code, do: types
|
||||
exported_types = List.flatten(exported_types)
|
||||
|
||||
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code, kind in [:opaque, :type] do
|
||||
cond do
|
||||
kind == :opaque -> {:opaque, type}
|
||||
{name, length(args)} in exported_types -> {:type, type}
|
||||
true -> {:typep, type}
|
||||
end
|
||||
end
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all specs available from the module's beam code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding beam file which can be
|
||||
located by the runtime system.
|
||||
"""
|
||||
@spec beam_specs(module | binary) :: [tuple] | nil
|
||||
def beam_specs(module) when is_atom(module) or is_binary(module) do
|
||||
from_abstract_code(module, :spec)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all callbacks available from the module's beam code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding beam file
|
||||
which can be located by the runtime system.
|
||||
"""
|
||||
@spec beam_callbacks(module | binary) :: [tuple] | nil
|
||||
def beam_callbacks(module) when is_atom(module) or is_binary(module) do
|
||||
from_abstract_code(module, :callback)
|
||||
end
|
||||
|
||||
defp from_abstract_code(module, kind) do
|
||||
case abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
for {:attribute, _, abs_kind, value} <- abstract_code, kind == abs_kind, do: value
|
||||
:error ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp abstract_code(module) do
|
||||
case :beam_lib.chunks(abstract_code_beam(module), [:abstract_code]) do
|
||||
{:ok, {_, [{:abstract_code, {_raw_abstract_v1, abstract_code}}]}} ->
|
||||
{:ok, abstract_code}
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp abstract_code_beam(module) when is_atom(module) do
|
||||
case :code.get_object_code(module) do
|
||||
{^module, beam, _filename} -> beam
|
||||
:error -> module
|
||||
end
|
||||
end
|
||||
|
||||
defp abstract_code_beam(binary) when is_binary(binary) do
|
||||
binary
|
||||
end
|
||||
|
||||
## Macro callbacks
|
||||
|
||||
@doc false
|
||||
def deftype(kind, {:::, _, [{name, _, args}, definition]}, caller) when is_atom(name) and name != ::: do
|
||||
args =
|
||||
if is_atom(args) do
|
||||
[]
|
||||
else
|
||||
for(arg <- args, do: variable(arg))
|
||||
end
|
||||
|
||||
vars = for {:var, _, var} <- args, do: var
|
||||
spec = typespec(definition, vars, caller)
|
||||
|
||||
vars = for {:var, _, _} = var <- args, do: var
|
||||
type = {name, spec, vars}
|
||||
|
||||
define_type(caller, kind, type)
|
||||
end
|
||||
|
||||
def deftype(_kind, other, caller) do
|
||||
type_spec = Macro.to_string(other)
|
||||
compile_error caller, "invalid type specification: #{type_spec}"
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
def defspec(type, {:when, _meta, [spec, guard]}, caller) do
|
||||
defspec(type, spec, guard, caller)
|
||||
end
|
||||
|
||||
def defspec(type, spec, caller) do
|
||||
defspec(type, spec, [], caller)
|
||||
end
|
||||
|
||||
defp defspec(type, {:::, meta, [{name, _, args}, return]}, guard, caller) when is_atom(name) and name != ::: do
|
||||
if is_atom(args), do: args = []
|
||||
|
||||
unless Keyword.keyword?(guard) do
|
||||
guard = Macro.to_string(guard)
|
||||
compile_error caller, "expected keywords as guard in function type specification, got: #{guard}"
|
||||
end
|
||||
|
||||
vars = Keyword.keys(guard)
|
||||
constraints = guard_to_constraints(guard, vars, meta, caller)
|
||||
|
||||
spec = {:type, line(meta), :fun, fn_args(meta, args, return, vars, caller)}
|
||||
if constraints != [] do
|
||||
spec = {:type, line(meta), :bounded_fun, [spec, constraints]}
|
||||
end
|
||||
|
||||
code = {{name, Kernel.length(args)}, spec}
|
||||
Module.compile_typespec(caller.module, type, code)
|
||||
code
|
||||
end
|
||||
|
||||
defp defspec(_type, spec, _guard, caller) do
|
||||
spec = Macro.to_string(spec)
|
||||
compile_error caller, "invalid function type specification: #{spec}"
|
||||
end
|
||||
|
||||
defp guard_to_constraints(guard, vars, meta, caller) do
|
||||
line = line(meta)
|
||||
|
||||
Enum.reduce(guard, [], fn
|
||||
{_name, {:var, _, context}}, acc when is_atom(context) ->
|
||||
acc
|
||||
{name, type}, acc ->
|
||||
constraint = [{:atom, line, :is_subtype}, [{:var, line, name}, typespec(type, vars, caller)]]
|
||||
type = {:type, line, :constraint, constraint}
|
||||
[type|acc]
|
||||
end) |> Enum.reverse
|
||||
end
|
||||
|
||||
## To AST conversion
|
||||
|
||||
defp collect_vars({:ann_type, _line, args}) when is_list(args) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp collect_vars({:type, _line, _kind, args}) when is_list(args) do
|
||||
Enum.flat_map(args, &collect_vars/1)
|
||||
end
|
||||
|
||||
defp collect_vars({:remote_type, _line, args}) when is_list(args) do
|
||||
Enum.flat_map(args, &collect_vars/1)
|
||||
end
|
||||
|
||||
defp collect_vars({:typed_record_field, _line, type}) do
|
||||
collect_vars(type)
|
||||
end
|
||||
|
||||
defp collect_vars({:paren_type, _line, [type]}) do
|
||||
collect_vars(type)
|
||||
end
|
||||
|
||||
defp collect_vars({:var, _line, var}) do
|
||||
[erl_to_ex_var(var)]
|
||||
end
|
||||
|
||||
defp collect_vars(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :tuple, :any}) do
|
||||
{:tuple, [line: line], []}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :tuple, args}) do
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
{:{}, [line: line], args}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, _line, :list, [{:type, _, :union, unions} = arg]}) do
|
||||
case unpack_typespec_kw(unions, []) do
|
||||
{:ok, ast} -> ast
|
||||
:error -> [typespec_to_ast(arg)]
|
||||
end
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, _line, :list, args}) do
|
||||
for arg <- args, do: typespec_to_ast(arg)
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :map, fields}) do
|
||||
fields = Enum.map fields, fn {:type, _, :map_field_assoc, k, v} ->
|
||||
{typespec_to_ast(k), typespec_to_ast(v)}
|
||||
end
|
||||
|
||||
{struct, fields} = Keyword.pop(fields, :__struct__)
|
||||
map = {:%{}, [line: line], fields}
|
||||
|
||||
if struct do
|
||||
{:%, [line: line], [struct, map]}
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :binary, [arg1, arg2]}) do
|
||||
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_ast(arg)
|
||||
cond do
|
||||
arg2 == 0 ->
|
||||
quote line: line, do: <<_ :: unquote(arg1)>>
|
||||
arg1 == 0 ->
|
||||
quote line: line, do: <<_ :: _ * unquote(arg2)>>
|
||||
true ->
|
||||
quote line: line, do: <<_ :: unquote(arg1) * unquote(arg2)>>
|
||||
end
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :union, args}) do
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
Enum.reduce Enum.reverse(args), fn(arg, expr) -> {:|, [line: line], [arg, expr]} end
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :fun, [{:type, _, :product, args}, result]}) do
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
[{:->, [line: line], [args, typespec_to_ast(result)]}]
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :fun, [args, result]}) do
|
||||
[{:->, [line: line], [[typespec_to_ast(args)], typespec_to_ast(result)]}]
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :fun, []}) do
|
||||
typespec_to_ast({:type, line, :fun, [{:type, line, :any}, {:type, line, :any, []} ]})
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, :range, [left, right]}) do
|
||||
{:"..", [line: line], [typespec_to_ast(left), typespec_to_ast(right)]}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, line, name, args}) do
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
{name, [line: line], args}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:var, line, var}) do
|
||||
{erl_to_ex_var(var), line, nil}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:op, line, op, arg}) do
|
||||
{op, [line: line], [typespec_to_ast(arg)]}
|
||||
end
|
||||
|
||||
# Special shortcut(s)
|
||||
defp typespec_to_ast({:remote_type, line, [{:atom, _, :elixir}, {:atom, _, :char_list}, []]}) do
|
||||
typespec_to_ast({:type, line, :char_list, []})
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:remote_type, line, [{:atom, _, :elixir}, {:atom, _, :char_data}, []]}) do
|
||||
typespec_to_ast({:type, line, :char_data, []})
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:remote_type, line, [{:atom, _, :elixir}, {:atom, _, :as_boolean}, [arg]]}) do
|
||||
typespec_to_ast({:type, line, :as_boolean, [arg]})
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:remote_type, line, [mod, name, args]}) do
|
||||
args = for arg <- args, do: typespec_to_ast(arg)
|
||||
dot = {:., [line: line], [typespec_to_ast(mod), typespec_to_ast(name)]}
|
||||
{dot, [line: line], args}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:ann_type, line, [var, type]}) do
|
||||
{:::, [line: line], [typespec_to_ast(var), typespec_to_ast(type)]}
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:typed_record_field,
|
||||
{:record_field, line, {:atom, line1, name}},
|
||||
type}) do
|
||||
typespec_to_ast({:ann_type, line, [{:var, line1, name}, type]})
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:type, _, :any}) do
|
||||
quote do: ...
|
||||
end
|
||||
|
||||
defp typespec_to_ast({:paren_type, _, [type]}) do
|
||||
typespec_to_ast(type)
|
||||
end
|
||||
|
||||
defp typespec_to_ast({t, _line, atom}) when is_atom(t) do
|
||||
atom
|
||||
end
|
||||
|
||||
defp typespec_to_ast(other), do: other
|
||||
|
||||
defp erl_to_ex_var(var) do
|
||||
case atom_to_binary(var) do
|
||||
<<"_", c :: [binary, size(1)], rest :: binary>> ->
|
||||
binary_to_atom("_#{String.downcase(c)}#{rest}")
|
||||
<<c :: [binary, size(1)], rest :: binary>> ->
|
||||
binary_to_atom("#{String.downcase(c)}#{rest}")
|
||||
end
|
||||
end
|
||||
|
||||
## From AST conversion
|
||||
|
||||
defp line(meta) do
|
||||
case :lists.keyfind(:line, 1, meta) do
|
||||
{:line, line} -> line
|
||||
false -> 0
|
||||
end
|
||||
end
|
||||
|
||||
# Handle unions
|
||||
defp typespec({:|, meta, [_, _]} = exprs, vars, caller) do
|
||||
exprs = collect_union(exprs)
|
||||
union = for e <- exprs, do: typespec(e, vars, caller)
|
||||
{:type, line(meta), :union, union}
|
||||
end
|
||||
|
||||
# Handle binaries
|
||||
defp typespec({:<<>>, meta, []}, _, _) do
|
||||
{:type, line(meta), :binary, [{:integer, line(meta), 0}, {:integer, line(meta), 0}]}
|
||||
end
|
||||
|
||||
defp typespec({:<<>>, meta, [{:::, _, [{:_, meta1, atom}, {:*, _, [{:_, meta2, atom}, unit]}]}]}, _, _) when is_atom(atom) do
|
||||
{:type, line(meta), :binary, [{:integer, line(meta1), 0}, {:integer, line(meta2), unit}]}
|
||||
end
|
||||
|
||||
defp typespec({:<<>>, meta, [{:::, meta1, [{:_, meta2, atom}, base]}]}, _, _) when is_atom(atom) do
|
||||
{:type, line(meta), :binary, [{:integer, line(meta1), base}, {:integer, line(meta2), 0}]}
|
||||
end
|
||||
|
||||
## Handle maps and structs
|
||||
defp typespec({:%{}, meta, fields}, vars, caller) do
|
||||
fields = Enum.map(fields, fn {k, v} ->
|
||||
{:type, line(meta), :map_field_assoc, typespec(k, vars, caller), typespec(v, vars, caller)}
|
||||
end)
|
||||
{:type, line(meta), :map, fields}
|
||||
end
|
||||
|
||||
defp typespec({:%, _, [name, {:%{}, meta, fields}]}, vars, caller) do
|
||||
typespec({:%{}, meta, [{:__struct__, name}|fields]}, vars, caller)
|
||||
end
|
||||
|
||||
# Handle ranges
|
||||
defp typespec({:.., meta, args}, vars, caller) do
|
||||
typespec({:range, meta, args}, vars, caller)
|
||||
end
|
||||
|
||||
# Handle special forms
|
||||
defp typespec({:__MODULE__, _, atom}, vars, caller) when is_atom(atom) do
|
||||
typespec(caller.module, vars, caller)
|
||||
end
|
||||
|
||||
defp typespec({:__aliases__, _, _} = alias, vars, caller) do
|
||||
atom = Macro.expand alias, caller
|
||||
typespec(atom, vars, caller)
|
||||
end
|
||||
|
||||
# Handle funs
|
||||
defp typespec([{:->, meta, [arguments, return]}], vars, caller) when is_list(arguments) do
|
||||
args = fn_args(meta, arguments, return, vars, caller)
|
||||
{:type, line(meta), :fun, args}
|
||||
end
|
||||
|
||||
# Handle type operator
|
||||
defp typespec({:::, meta, [var, expr]}, vars, caller) do
|
||||
left = typespec(var, [elem(var, 0)|vars], caller)
|
||||
right = typespec(expr, vars, caller)
|
||||
{:ann_type, line(meta), [left, right]}
|
||||
end
|
||||
|
||||
# Handle unary ops
|
||||
defp typespec({op, meta, [integer]}, _, _) when op in [:+, :-] and is_integer(integer) do
|
||||
{:op, line(meta), op, {:integer, line(meta), integer}}
|
||||
end
|
||||
|
||||
# Handle access macro
|
||||
defp typespec({{:., meta, [Kernel, :access]}, meta1, [target, args]}, vars, caller) do
|
||||
access = {{:., meta, [Kernel, :access]}, meta1,
|
||||
[target, args ++ [_: {:any, [], []}]]}
|
||||
typespec(Macro.expand(access, caller), vars, caller)
|
||||
end
|
||||
|
||||
# Handle remote calls
|
||||
defp typespec({{:., meta, [remote, name]}, _, args} = orig, vars, caller) do
|
||||
remote = Macro.expand remote, caller
|
||||
unless is_atom(remote) do
|
||||
compile_error(caller, "invalid remote in typespec: #{Macro.to_string(orig)}")
|
||||
end
|
||||
remote_type({typespec(remote, vars, caller), meta, typespec(name, vars, caller), args}, vars, caller)
|
||||
end
|
||||
|
||||
# Handle tuples
|
||||
defp typespec({:tuple, meta, args}, _vars, _caller) when args == [] or is_atom(args) do
|
||||
{:type, line(meta), :tuple, :any}
|
||||
end
|
||||
|
||||
defp typespec({:{}, meta, t}, vars, caller) when is_list(t) do
|
||||
args = for e <- t, do: typespec(e, vars, caller)
|
||||
{:type, line(meta), :tuple, args}
|
||||
end
|
||||
|
||||
defp typespec({left, right}, vars, caller) do
|
||||
typespec({:{}, [], [left, right]}, vars, caller)
|
||||
end
|
||||
|
||||
# Handle blocks
|
||||
defp typespec({:__block__, _meta, [arg]}, vars, caller) do
|
||||
typespec(arg, vars, caller)
|
||||
end
|
||||
|
||||
# Handle variables or local calls
|
||||
defp typespec({name, meta, atom}, vars, caller) when is_atom(atom) do
|
||||
if name in vars do
|
||||
{:var, line(meta), name}
|
||||
else
|
||||
typespec({name, meta, []}, vars, caller)
|
||||
end
|
||||
end
|
||||
|
||||
# Handle local calls
|
||||
defp typespec({:string, meta, arguments}, vars, caller) do
|
||||
:elixir_errors.warn caller.line, caller.file, "string() type use is discouraged. For character lists, use " <>
|
||||
"char_list() type, for strings, String.t()\n#{Exception.format_stacktrace(Macro.Env.stacktrace(caller))}"
|
||||
arguments = for arg <- arguments, do: typespec(arg, vars, caller)
|
||||
{:type, line(meta), :string, arguments}
|
||||
end
|
||||
|
||||
defp typespec({:char_list, _meta, []}, vars, caller) do
|
||||
typespec((quote do: :elixir.char_list()), vars, caller)
|
||||
end
|
||||
|
||||
defp typespec({:char_data, _meta, []}, vars, caller) do
|
||||
typespec((quote do: :elixir.char_data()), vars, caller)
|
||||
end
|
||||
|
||||
defp typespec({:as_boolean, _meta, [arg]}, vars, caller) do
|
||||
typespec((quote do: :elixir.as_boolean(unquote(arg))), vars, caller)
|
||||
end
|
||||
|
||||
defp typespec({name, meta, arguments}, vars, caller) do
|
||||
arguments = for arg <- arguments, do: typespec(arg, vars, caller)
|
||||
{:type, line(meta), name, arguments}
|
||||
end
|
||||
|
||||
# Handle literals
|
||||
defp typespec(atom, _, _) when is_atom(atom) do
|
||||
{:atom, 0, atom}
|
||||
end
|
||||
|
||||
defp typespec(integer, _, _) when is_integer(integer) do
|
||||
{:integer, 0, integer}
|
||||
end
|
||||
|
||||
defp typespec([], vars, caller) do
|
||||
typespec({nil, [], []}, vars, caller)
|
||||
end
|
||||
|
||||
defp typespec([spec], vars, caller) do
|
||||
typespec({:list, [], [spec]}, vars, caller)
|
||||
end
|
||||
|
||||
defp typespec([spec, {:"...", _, quoted}], vars, caller) when is_atom(quoted) do
|
||||
typespec({:nonempty_list, [], [spec]}, vars, caller)
|
||||
end
|
||||
|
||||
defp typespec(list, vars, caller) do
|
||||
[h|t] = Enum.reverse(list)
|
||||
union = Enum.reduce(t, validate_kw(h, list, caller), fn(x, acc) ->
|
||||
{:|, [], [validate_kw(x, list, caller), acc]}
|
||||
end)
|
||||
typespec({:list, [], [union]}, vars, caller)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp compile_error(caller, desc) do
|
||||
raise CompileError, file: caller.file, line: caller.line, description: desc
|
||||
end
|
||||
|
||||
defp remote_type({remote, meta, name, arguments}, vars, caller) do
|
||||
arguments = for arg <- arguments, do: typespec(arg, vars, caller)
|
||||
{:remote_type, line(meta), [ remote, name, arguments ]}
|
||||
end
|
||||
|
||||
defp collect_union({:|, _, [a, b]}), do: [a|collect_union(b)]
|
||||
defp collect_union(v), do: [v]
|
||||
|
||||
defp validate_kw({key, _} = t, _, _caller) when is_atom(key), do: t
|
||||
defp validate_kw(_, original, caller) do
|
||||
compile_error(caller, "unexpected list in typespec: #{Macro.to_string original}")
|
||||
end
|
||||
|
||||
defp fn_args(meta, args, return, vars, caller) do
|
||||
case [fn_args(meta, args, vars, caller), typespec(return, vars, caller)] do
|
||||
[{:type, _, :any}, {:type, _, :any, []}] -> []
|
||||
x -> x
|
||||
end
|
||||
end
|
||||
|
||||
defp fn_args(meta, [{:"...", _, _}], _vars, _caller) do
|
||||
{:type, line(meta), :any}
|
||||
end
|
||||
|
||||
defp fn_args(meta, args, vars, caller) do
|
||||
args = for arg <- args, do: typespec(arg, vars, caller)
|
||||
{:type, line(meta), :product, args}
|
||||
end
|
||||
|
||||
defp variable({name, meta, _}) do
|
||||
{:var, line(meta), name}
|
||||
end
|
||||
|
||||
defp unpack_typespec_kw([{:type, _, :tuple, [{:atom, _, atom}, type]}|t], acc) do
|
||||
unpack_typespec_kw(t, [{atom, typespec_to_ast(type)}|acc])
|
||||
end
|
||||
|
||||
defp unpack_typespec_kw([], acc) do
|
||||
{:ok, :lists.reverse(acc)}
|
||||
end
|
||||
|
||||
defp unpack_typespec_kw(_, _acc) do
|
||||
:error
|
||||
end
|
||||
end
|
||||
@@ -1,577 +0,0 @@
|
||||
defmodule Keyword do
|
||||
@moduledoc """
|
||||
A keyword is a list of tuples where the first element
|
||||
of the tuple is an atom and the second element can be
|
||||
any value.
|
||||
|
||||
A keyword may have duplicated keys so it is not strictly
|
||||
a dictionary. However most of the functions in this module
|
||||
behaves exactly as a dictionary and mimic the API defined
|
||||
by the `Dict` behaviour.
|
||||
|
||||
For example, `Keyword.get` will get the first entry matching
|
||||
the given key, regardless if duplicated entries exist.
|
||||
Similarly, `Keyword.put` and `Keyword.delete` ensure all
|
||||
duplicated entries for a given key are removed when invoked.
|
||||
A handful of functions exist to handle duplicated keys, in
|
||||
particular, `from_enum` allows creating a new keywords without
|
||||
removing duplicated keys, `get_values` returns all values for
|
||||
a given key and `delete_first` deletes just one of the existing
|
||||
entries.
|
||||
|
||||
Since a keyword list is simply a list, all the operations defined
|
||||
in `Enum` and `List` can also be applied.
|
||||
"""
|
||||
|
||||
@behaviour Dict
|
||||
|
||||
@type key :: atom
|
||||
@type value :: any
|
||||
|
||||
@type t :: [{key, value}]
|
||||
@type t(value) :: [{key, value}]
|
||||
|
||||
@doc """
|
||||
Checks if the given argument is a keywords list or not.
|
||||
"""
|
||||
@spec keyword?(term) :: boolean
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
keyword?(rest)
|
||||
end
|
||||
|
||||
def keyword?([]), do: true
|
||||
def keyword?(_other), do: false
|
||||
|
||||
@doc """
|
||||
Returns an empty keyword list, i.e. an empty list.
|
||||
"""
|
||||
@spec new :: t
|
||||
def new do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a keyword from an enumerable.
|
||||
|
||||
Duplicated entries are removed, the latest one prevails.
|
||||
I.e. differently from `Enum.into(enumerable, [])`,
|
||||
`Keyword.new(enumerable)` guarantees the keys are unique.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.new([{:b, 1}, {:a, 2}])
|
||||
[a: 2, b: 1]
|
||||
|
||||
"""
|
||||
@spec new(Enum.t) :: t
|
||||
def new(pairs) do
|
||||
Enum.reduce pairs, [], fn {k, v}, keywords ->
|
||||
put(keywords, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a keyword from an enumerable via the transformation function.
|
||||
|
||||
Duplicated entries are removed, the latest one prevails.
|
||||
I.e. differently from `Enum.into(enumerable, [], fun)`,
|
||||
`Keyword.new(enumerable, fun)` guarantees the keys are unique.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.new([:a, :b], fn (x) -> {x, x} end) |> Enum.sort
|
||||
[a: :a, b: :b]
|
||||
|
||||
"""
|
||||
@spec new(Enum.t, ({key, value} -> {key, value})) :: t
|
||||
def new(pairs, transform) do
|
||||
Enum.reduce pairs, [], fn i, keywords ->
|
||||
{k, v} = transform.(i)
|
||||
put(keywords, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for a specific `key`.
|
||||
|
||||
If `key` does not exist, return default value (`nil` if no default value).
|
||||
|
||||
If duplicated entries exist, the first one is returned.
|
||||
Use `get_values/2` to retrieve all entries.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.get([a: 1], :a)
|
||||
1
|
||||
|
||||
iex> Keyword.get([a: 1], :b)
|
||||
nil
|
||||
|
||||
iex> Keyword.get([a: 1], :b, 3)
|
||||
3
|
||||
|
||||
"""
|
||||
@spec get(t, key) :: value
|
||||
@spec get(t, key, value) :: value
|
||||
def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> value
|
||||
false -> default
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the value for a specific `key` and returns it in a tuple.
|
||||
If the `key` does not exist, returns `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.fetch([a: 1], :a)
|
||||
{:ok, 1}
|
||||
|
||||
iex> Keyword.fetch([a: 1], :b)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec fetch(t, key) :: {:ok, value}
|
||||
def fetch(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {:ok, value}
|
||||
false -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the value for specific `key`. If `key` does not exist,
|
||||
a `KeyError` is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.fetch!([a: 1], :a)
|
||||
1
|
||||
|
||||
iex> Keyword.fetch!([a: 1], :b)
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@spec fetch!(t, key) :: value | no_return
|
||||
def fetch!(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> value
|
||||
false -> raise(KeyError, key: key, term: keywords)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all values for a specific `key`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.get_values([a: 1, a: 2], :a)
|
||||
[1,2]
|
||||
|
||||
"""
|
||||
@spec get_values(t, key) :: [value]
|
||||
def get_values(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
for {k, v} <- keywords, key == k, do: v
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all keys from the keyword list. Duplicated
|
||||
keys appear duplicated in the final list of keys.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.keys([a: 1, b: 2])
|
||||
[:a,:b]
|
||||
|
||||
iex> Keyword.keys([a: 1, b: 2, a: 3])
|
||||
[:a,:b,:a]
|
||||
|
||||
"""
|
||||
@spec keys(t) :: [key]
|
||||
def keys(keywords) when is_list(keywords) do
|
||||
for {key, _} <- keywords, do: key
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all values from the keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.values([a: 1, b: 2])
|
||||
[1,2]
|
||||
|
||||
"""
|
||||
@spec values(t) :: [value]
|
||||
def values(keywords) when is_list(keywords) do
|
||||
for {_, value} <- keywords, do: value
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the entry in the keyword list for a `key` with `value`.
|
||||
If no `key` with `value` exists, returns the keyword list unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2], :a, 1)
|
||||
[b: 2]
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2, a: 3], :a, 3)
|
||||
[a: 1, b: 2]
|
||||
|
||||
iex> Keyword.delete([b: 2], :a, 5)
|
||||
[b: 2]
|
||||
|
||||
"""
|
||||
@spec delete(t, key, value) :: t
|
||||
def delete(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
for {k, v} = tuple <- keywords, key != k or value != v, do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all entries in the keyword list for a specific `key`.
|
||||
If the `key` does not exist, returns the keyword list unchanged.
|
||||
Use `delete_first` to delete just the first entry in case of
|
||||
duplicated keys.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2], :a)
|
||||
[b: 2]
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2, a: 3], :a)
|
||||
[b: 2]
|
||||
|
||||
iex> Keyword.delete([b: 2], :a)
|
||||
[b: 2]
|
||||
|
||||
"""
|
||||
@spec delete(t, key) :: t
|
||||
def delete(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
for {k, _} = tuple <- keywords, key != k, do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the first entry in the keyword list for a specific `key`.
|
||||
If the `key` does not exist, returns the keyword list unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete_first([a: 1, b: 2, a: 3], :a)
|
||||
[b: 2, a: 3]
|
||||
|
||||
iex> Keyword.delete_first([b: 2], :a)
|
||||
[b: 2]
|
||||
|
||||
"""
|
||||
@spec delete_first(t, key) :: t
|
||||
def delete_first(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
:lists.keydelete(key, 1, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key`.
|
||||
|
||||
If a previous value is already stored, all entries are
|
||||
removed and the value is overridden.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.put([a: 1, b: 2], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
|
||||
iex> Keyword.put([a: 1, b: 2, a: 4], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
|
||||
"""
|
||||
@spec put(t, key, value) :: t
|
||||
def put(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
[{key, value}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` unless the entry `key`
|
||||
already exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.put_new([a: 1], :b, 2)
|
||||
[b: 2, a: 1]
|
||||
|
||||
iex> Keyword.put_new([a: 1, b: 2], :a, 3)
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec put_new(t, key, value) :: t
|
||||
def put_new(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, _} -> keywords
|
||||
false -> [{key, value}|keywords]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if two keywords are equal. I.e. they contain
|
||||
the same keys and those keys contain the same values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.equal?([a: 1, b: 2], [b: 2, a: 1])
|
||||
true
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(left, right) when is_list(left) and is_list(right) do
|
||||
:lists.sort(left) == :lists.sort(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two keyword lists into one. If they have duplicated
|
||||
entries, the one given as second argument wins.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4]) |> Enum.sort
|
||||
[a: 3, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
@spec merge(t, t) :: t
|
||||
def merge(d1, d2) when is_list(d1) and is_list(d2) do
|
||||
d2 ++ for({k, _} = tuple <- d1, not has_key?(d2, k), do: tuple)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two keyword lists into one. If they have duplicated
|
||||
entries, the given function is invoked to solve conflicts.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4], fn (_k, v1, v2) ->
|
||||
...> v1 + v2
|
||||
...> end)
|
||||
[a: 4, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
@spec merge(t, t, (key, value, value -> value)) :: t
|
||||
def merge(d1, d2, fun) when is_list(d1) and is_list(d2) do
|
||||
do_merge(d2, d1, fun)
|
||||
end
|
||||
|
||||
defp do_merge([{k, v2}|t], acc, fun) do
|
||||
do_merge t, update(acc, k, v2, fn(v1) -> fun.(k, v1, v2) end), fun
|
||||
end
|
||||
|
||||
defp do_merge([], acc, _fun) do
|
||||
acc
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether a given `key` exists in the given `keywords`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.has_key?([a: 1], :a)
|
||||
true
|
||||
|
||||
iex> Keyword.has_key?([a: 1], :b)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec has_key?(t, key) :: boolean
|
||||
def has_key?(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
:lists.keymember(key, 1, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the `key` with the given function. If the `key` does
|
||||
not exist, raises `KeyError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update!([a: 1], :a, &(&1 * 2))
|
||||
[a: 2]
|
||||
|
||||
iex> Keyword.update!([a: 1], :b, &(&1 * 2))
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@spec update!(t, key, (value -> value)) :: t | no_return
|
||||
def update!(keywords, key, fun) do
|
||||
update!(keywords, key, fun, keywords)
|
||||
end
|
||||
|
||||
defp update!([{key, value}|keywords], key, fun, _dict) do
|
||||
[{key, fun.(value)}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp update!([{_, _} = e|keywords], key, fun, dict) do
|
||||
[e|update!(keywords, key, fun, dict)]
|
||||
end
|
||||
|
||||
defp update!([], key, _fun, dict) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the `key` with the given function. If the `key` does
|
||||
not exist, inserts the given `initial` value.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
|
||||
[a: 2]
|
||||
|
||||
iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
|
||||
[a: 1, b: 11]
|
||||
|
||||
"""
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update([{key, value}|keywords], key, _initial, fun) do
|
||||
[{key, fun.(value)}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
def update([{_, _} = e|keywords], key, initial, fun) do
|
||||
[e|update(keywords, key, initial, fun)]
|
||||
end
|
||||
|
||||
def update([], key, initial, _fun) when is_atom(key) do
|
||||
[{key, initial}]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits the given keywords in two given the given keys.
|
||||
Duplicated keys are preserved in the split keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4]
|
||||
iex> Keyword.split(d, [:a, :c, :e])
|
||||
{[a: 1, c: 3], [b: 2, d: 4]}
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4, a: 5]
|
||||
iex> Keyword.split(d, [:a, :c, :e])
|
||||
{[a: 1, c: 3, a: 5], [b: 2, d: 4]}
|
||||
|
||||
"""
|
||||
def split(keywords, keys) when is_list(keywords) do
|
||||
acc = {[], []}
|
||||
|
||||
{take, drop} = Enum.reduce keywords, acc, fn({k, v}, {take, drop}) ->
|
||||
case k in keys do
|
||||
true -> {[{k, v}|take], drop}
|
||||
false -> {take, [{k, v}|drop]}
|
||||
end
|
||||
end
|
||||
|
||||
{Enum.reverse(take), Enum.reverse(drop)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes the given keys from the dict.
|
||||
Duplicated keys are preserved in the new keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4]
|
||||
iex> Keyword.take(d, [:a, :c, :e])
|
||||
[a: 1, c: 3]
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4, a: 5]
|
||||
iex> Keyword.take(d, [:a, :c, :e])
|
||||
[a: 1, c: 3, a: 5]
|
||||
|
||||
"""
|
||||
def take(keywords, keys) when is_list(keywords) do
|
||||
for {k, _} = tuple <- keywords, k in keys, do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Drops the given keys from the dict.
|
||||
Duplicated keys are preserved in the new keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4]
|
||||
iex> Keyword.drop(d, [:b, :d])
|
||||
[a: 1, c: 3]
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4, a: 5]
|
||||
iex> Keyword.drop(d, [:b, :d])
|
||||
[a: 1, c: 3, a: 5]
|
||||
|
||||
"""
|
||||
def drop(keywords, keys) when is_list(keywords) do
|
||||
for {k, _} = tuple <- keywords, not k in keys, do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value associated with `key` in the keyword
|
||||
list as well as the keyword list without `key`.
|
||||
|
||||
All duplicated entries are removed. See `pop_first/3` for
|
||||
removing only the first entry.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.pop [a: 1], :a
|
||||
{1,[]}
|
||||
|
||||
iex> Keyword.pop [a: 1], :b
|
||||
{nil,[a: 1]}
|
||||
|
||||
iex> Keyword.pop [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop [a: 1, a: 2], :a
|
||||
{1,[]}
|
||||
|
||||
"""
|
||||
def pop(keywords, key, default \\ nil) when is_list(keywords) do
|
||||
{get(keywords, key, default), delete(keywords, key)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value associated with `key` in the keyword
|
||||
list as well as the keyword list without that particular ocurrence
|
||||
of `key`.
|
||||
|
||||
Duplicated entries are not removed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :a
|
||||
{1,[]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :b
|
||||
{nil,[a: 1]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1, a: 2], :a
|
||||
{1,[a: 2]}
|
||||
|
||||
"""
|
||||
def pop_first(keywords, key, default \\ nil) when is_list(keywords) do
|
||||
{get(keywords, key, default), delete_first(keywords, key)}
|
||||
end
|
||||
|
||||
# Dict callbacks
|
||||
|
||||
@doc false
|
||||
def size(keyword) do
|
||||
length(keyword)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def to_list(keyword) do
|
||||
keyword
|
||||
end
|
||||
end
|
||||
@@ -1,613 +0,0 @@
|
||||
defmodule List do
|
||||
@moduledoc """
|
||||
Implements functions that only make sense for lists
|
||||
and cannot be part of the Enum protocol. In general,
|
||||
favor using the Enum API instead of List.
|
||||
|
||||
Some functions in this module expect an index. Index
|
||||
access for list is linear. Negative indexes are also
|
||||
supported but they imply the list will be iterated twice,
|
||||
one to calculate the proper index and another to the
|
||||
operation.
|
||||
|
||||
A decision was taken to delegate most functions to
|
||||
Erlang's standard library but follow Elixir's convention
|
||||
of receiving the target (in this case, a list) as the
|
||||
first argument.
|
||||
"""
|
||||
|
||||
@compile :inline_list_funcs
|
||||
|
||||
@doc """
|
||||
Deletes the given item from the list. Returns a list without
|
||||
the item. If the item occurs more than once in the list, just
|
||||
the first occurrence is removed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.delete([1, 2, 3], 1)
|
||||
[2,3]
|
||||
|
||||
iex> List.delete([1, 2, 2, 3], 2)
|
||||
[1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec delete(list, any) :: list
|
||||
def delete(list, item) do
|
||||
:lists.delete(item, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Duplicates the given element `n` times in a list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.duplicate("hello", 3)
|
||||
["hello","hello","hello"]
|
||||
|
||||
iex> List.duplicate([1, 2], 2)
|
||||
[[1,2],[1,2]]
|
||||
|
||||
"""
|
||||
@spec duplicate(elem, non_neg_integer) :: [elem] when elem: var
|
||||
def duplicate(elem, n) do
|
||||
:lists.duplicate(n, elem)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.flatten([1, [[2], 3]])
|
||||
[1,2,3]
|
||||
|
||||
"""
|
||||
@spec flatten(deep_list) :: list when deep_list: [any | deep_list]
|
||||
def flatten(list) do
|
||||
:lists.flatten(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
The list `tail` will be added at the end of
|
||||
the flattened list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.flatten([1, [[2], 3]], [4, 5])
|
||||
[1,2,3,4,5]
|
||||
|
||||
"""
|
||||
@spec flatten(deep_list, [elem]) :: [elem] when elem: var, deep_list: [elem | deep_list]
|
||||
def flatten(list, tail) do
|
||||
:lists.flatten(list, tail)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Folds (reduces) the given list to the left with
|
||||
a function. Requires an accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.foldl([5, 5], 10, fn (x, acc) -> x + acc end)
|
||||
20
|
||||
|
||||
iex> List.foldl([1, 2, 3, 4], 0, fn (x, acc) -> x - acc end)
|
||||
2
|
||||
|
||||
"""
|
||||
@spec foldl([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var
|
||||
def foldl(list, acc, function) when is_list(list) and is_function(function) do
|
||||
:lists.foldl(function, acc, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Folds (reduces) the given list to the right with
|
||||
a function. Requires an accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.foldr([1, 2, 3, 4], 0, fn (x, acc) -> x - acc end)
|
||||
-2
|
||||
|
||||
"""
|
||||
@spec foldr([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var
|
||||
def foldr(list, acc, function) when is_list(list) and is_function(function) do
|
||||
:lists.foldr(function, acc, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first element in `list` or `nil` if `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.first([])
|
||||
nil
|
||||
|
||||
iex> List.first([1])
|
||||
1
|
||||
|
||||
iex> List.first([1, 2, 3])
|
||||
1
|
||||
|
||||
"""
|
||||
@spec first([elem]) :: nil | elem when elem: var
|
||||
def first([]), do: nil
|
||||
def first([h|_]), do: h
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list` or `nil` if `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.last([])
|
||||
nil
|
||||
|
||||
iex> List.last([1])
|
||||
1
|
||||
|
||||
iex> List.last([1, 2, 3])
|
||||
3
|
||||
|
||||
"""
|
||||
@spec last([elem]) :: nil | elem when elem: var
|
||||
def last([]), do: nil
|
||||
def last([h]), do: h
|
||||
def last([_|t]), do: last(t)
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns the first tuple
|
||||
where the item at `position` in the tuple matches the
|
||||
given `item`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keyfind([a: 1, b: 2], :a, 0)
|
||||
{:a, 1}
|
||||
|
||||
iex> List.keyfind([a: 1, b: 2], 2, 1)
|
||||
{:b, 2}
|
||||
|
||||
iex> List.keyfind([a: 1, b: 2], :c, 0)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec keyfind([tuple], any, non_neg_integer, any) :: any
|
||||
def keyfind(list, key, position, default \\ nil) do
|
||||
:lists.keyfind(key, position + 1, list) || default
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns `true` if there is
|
||||
a tuple where the item at `position` in the tuple matches
|
||||
the given `item`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keymember?([a: 1, b: 2], :a, 0)
|
||||
true
|
||||
|
||||
iex> List.keymember?([a: 1, b: 2], 2, 1)
|
||||
true
|
||||
|
||||
iex> List.keymember?([a: 1, b: 2], :c, 0)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec keymember?([tuple], any, non_neg_integer) :: any
|
||||
def keymember?(list, key, position) do
|
||||
:lists.keymember(key, position + 1, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and replaces the item
|
||||
identified by `key` at `position` if it exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keyreplace([a: 1, b: 2], :a, 0, {:a, 3})
|
||||
[a: 3, b: 2]
|
||||
|
||||
"""
|
||||
@spec keyreplace([tuple], any, non_neg_integer, tuple) :: [tuple]
|
||||
def keyreplace(list, key, position, new_tuple) do
|
||||
:lists.keyreplace(key, position + 1, list, new_tuple)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and sorts the items
|
||||
at `position` of the tuples. The sort is stable.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keysort([a: 5, b: 1, c: 3], 1)
|
||||
[b: 1, c: 3, a: 5]
|
||||
|
||||
iex> List.keysort([a: 5, c: 1, b: 3], 0)
|
||||
[a: 5, b: 3, c: 1]
|
||||
|
||||
"""
|
||||
@spec keysort([tuple], non_neg_integer) :: [tuple]
|
||||
def keysort(list, position) do
|
||||
:lists.keysort(position + 1, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and replaces the item
|
||||
identified by `key` at `position`. If the item
|
||||
does not exist, it is added to the end of the list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keystore([a: 1, b: 2], :a, 0, {:a, 3})
|
||||
[a: 3, b: 2]
|
||||
|
||||
iex> List.keystore([a: 1, b: 2], :c, 0, {:c, 3})
|
||||
[a: 1, b: 2, c: 3]
|
||||
|
||||
"""
|
||||
@spec keystore([tuple], any, non_neg_integer, tuple) :: [tuple]
|
||||
def keystore(list, key, position, new_tuple) do
|
||||
:lists.keystore(key, position + 1, list, new_tuple)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and deletes the first tuple
|
||||
where the item at `position` matches the
|
||||
given `item`. Returns the new list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keydelete([a: 1, b: 2], :a, 0)
|
||||
[b: 2]
|
||||
|
||||
iex> List.keydelete([a: 1, b: 2], 2, 1)
|
||||
[a: 1]
|
||||
|
||||
iex> List.keydelete([a: 1, b: 2], :c, 0)
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec keydelete([tuple], any, non_neg_integer) :: [tuple]
|
||||
def keydelete(list, key, position) do
|
||||
:lists.keydelete(key, position + 1, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps the argument in a list.
|
||||
If the argument is already a list, returns the list.
|
||||
If the argument is `nil`, returns an empty list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.wrap("hello")
|
||||
["hello"]
|
||||
|
||||
iex> List.wrap([1, 2, 3])
|
||||
[1,2,3]
|
||||
|
||||
iex> List.wrap(nil)
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec wrap(list | any) :: list
|
||||
def wrap(list) when is_list(list) do
|
||||
list
|
||||
end
|
||||
|
||||
def wrap(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
def wrap(other) do
|
||||
[other]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips corresponding elements from each list in `list_of_lists`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.zip([[1, 2], [3, 4], [5, 6]])
|
||||
[{1, 3, 5}, {2, 4, 6}]
|
||||
|
||||
iex> List.zip([[1, 2], [3], [5, 6]])
|
||||
[{1, 3, 5}]
|
||||
|
||||
"""
|
||||
@spec zip([list]) :: [tuple]
|
||||
def zip([]), do: []
|
||||
def zip(list_of_lists) when is_list(list_of_lists) do
|
||||
do_zip(list_of_lists, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unzips the given list of lists or tuples into separate lists and returns a
|
||||
list of lists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.unzip([{1, 2}, {3, 4}])
|
||||
[[1, 3], [2, 4]]
|
||||
|
||||
iex> List.unzip([{1, :a, "apple"}, {2, :b, "banana"}, {3, :c}])
|
||||
[[1, 2, 3], [:a, :b, :c]]
|
||||
|
||||
"""
|
||||
@spec unzip([tuple]) :: [list]
|
||||
def unzip(list) when is_list(list) do
|
||||
:lists.map &tuple_to_list/1, zip(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with `value` inserted at the specified `index`.
|
||||
Note that `index` is capped at the list length. Negative indices
|
||||
indicate an offset from the end of the list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.insert_at([1, 2, 3, 4], 2, 0)
|
||||
[1, 2, 0, 3, 4]
|
||||
|
||||
iex> List.insert_at([1, 2, 3], 10, 0)
|
||||
[1, 2, 3, 0]
|
||||
|
||||
iex> List.insert_at([1, 2, 3], -1, 0)
|
||||
[1, 2, 3, 0]
|
||||
|
||||
iex> List.insert_at([1, 2, 3], -10, 0)
|
||||
[0, 1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec insert_at(list, integer, any) :: list
|
||||
def insert_at(list, index, value) do
|
||||
if index < 0 do
|
||||
do_insert_at(list, length(list) + index + 1, value)
|
||||
else
|
||||
do_insert_at(list, index, value)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with a replaced value at the specified `index`.
|
||||
Negative indices indicate an offset from the end of the list.
|
||||
If `index` is out of bounds, the original `list` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.replace_at([1, 2, 3], 0, 0)
|
||||
[0, 2, 3]
|
||||
|
||||
iex> List.replace_at([1, 2, 3], 10, 0)
|
||||
[1, 2, 3]
|
||||
|
||||
iex> List.replace_at([1, 2, 3], -1, 0)
|
||||
[1, 2, 0]
|
||||
|
||||
iex> List.replace_at([1, 2, 3], -10, 0)
|
||||
[1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec replace_at(list, integer, any) :: list
|
||||
def replace_at(list, index, value) do
|
||||
if index < 0 do
|
||||
do_replace_at(list, length(list) + index, value)
|
||||
else
|
||||
do_replace_at(list, index, value)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with an updated value at the specified `index`.
|
||||
Negative indices indicate an offset from the end of the list.
|
||||
If `index` is out of bounds, the original `list` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.update_at([1, 2, 3], 0, &(&1 + 10))
|
||||
[11, 2, 3]
|
||||
|
||||
iex> List.update_at([1, 2, 3], 10, &(&1 + 10))
|
||||
[1, 2, 3]
|
||||
|
||||
iex> List.update_at([1, 2, 3], -1, &(&1 + 10))
|
||||
[1, 2, 13]
|
||||
|
||||
iex> List.update_at([1, 2, 3], -10, &(&1 + 10))
|
||||
[1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec update_at([elem], integer, (elem -> any)) :: list when elem: var
|
||||
def update_at(list, index, fun) do
|
||||
if index < 0 do
|
||||
do_update_at(list, length(list) + index, fun)
|
||||
else
|
||||
do_update_at(list, index, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Produces a new list by removing the value at the specified `index`.
|
||||
Negative indices indicate an offset from the end of the list.
|
||||
If `index` is out of bounds, the original `list` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.delete_at([1, 2, 3], 0)
|
||||
[2, 3]
|
||||
|
||||
iex List.delete_at([1, 2, 3], 10)
|
||||
[1, 2, 3]
|
||||
|
||||
iex> List.delete_at([1, 2, 3], -1)
|
||||
[1, 2]
|
||||
|
||||
"""
|
||||
@spec delete_at(list, integer) :: list
|
||||
def delete_at(list, index) do
|
||||
if index < 0 do
|
||||
do_delete_at(list, length(list) + index)
|
||||
else
|
||||
do_delete_at(list, index)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts char data into a char list.
|
||||
|
||||
A char data is a string or a list of integers and strings and it
|
||||
is converted into a list with each codepoint represented as its
|
||||
respective integer value.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.from_char_data("æß")
|
||||
{:ok, 'æß'}
|
||||
iex> List.from_char_data([?a, "bc"])
|
||||
{:ok, 'abc'}
|
||||
|
||||
"""
|
||||
@spec from_char_data(char_data) :: {:ok, char_list} | {:error, list, binary} | {:incomplete, list, binary}
|
||||
def from_char_data(char_data) do
|
||||
case :unicode.characters_to_list(char_data) do
|
||||
result when is_list(result) ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
{:incomplete, _, _} = incomplete ->
|
||||
incomplete
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts char data into a char list.
|
||||
|
||||
A char data is a string or a list of integers and strings and it
|
||||
is converted into a list with each codepoint represented as its
|
||||
respective integer value.
|
||||
|
||||
In case the conversion fails or is incomplete, it raises a
|
||||
`UnicodeConversionError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.from_char_data!("æß")
|
||||
'æß'
|
||||
iex> List.from_char_data!("abc")
|
||||
'abc'
|
||||
|
||||
"""
|
||||
@spec from_char_data!(char_data) :: char_list | no_return
|
||||
def from_char_data!(char_data) do
|
||||
case :unicode.characters_to_list(char_data) do
|
||||
result when is_list(result) ->
|
||||
result
|
||||
|
||||
{:error, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
|
||||
|
||||
{:incomplete, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
# replace_at
|
||||
|
||||
defp do_replace_at([], _index, _value) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp do_replace_at(list, index, _value) when index < 0 do
|
||||
list
|
||||
end
|
||||
|
||||
defp do_replace_at([_old|rest], 0, value) do
|
||||
[ value | rest ]
|
||||
end
|
||||
|
||||
defp do_replace_at([h|t], index, value) do
|
||||
[ h | do_replace_at(t, index - 1, value) ]
|
||||
end
|
||||
|
||||
# insert_at
|
||||
|
||||
defp do_insert_at([], _index, value) do
|
||||
[ value ]
|
||||
end
|
||||
|
||||
defp do_insert_at(list, index, value) when index <= 0 do
|
||||
[ value | list ]
|
||||
end
|
||||
|
||||
defp do_insert_at([h|t], index, value) do
|
||||
[ h | do_insert_at(t, index - 1, value) ]
|
||||
end
|
||||
|
||||
# update_at
|
||||
|
||||
defp do_update_at([value|list], 0, fun) do
|
||||
[ fun.(value) | list ]
|
||||
end
|
||||
|
||||
defp do_update_at(list, index, _fun) when index < 0 do
|
||||
list
|
||||
end
|
||||
|
||||
defp do_update_at([h|t], index, fun) do
|
||||
[ h | do_update_at(t, index - 1, fun) ]
|
||||
end
|
||||
|
||||
defp do_update_at([], _index, _fun) do
|
||||
[]
|
||||
end
|
||||
|
||||
# delete_at
|
||||
|
||||
defp do_delete_at([], _index) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp do_delete_at([_|t], 0) do
|
||||
t
|
||||
end
|
||||
|
||||
defp do_delete_at(list, index) when index < 0 do
|
||||
list
|
||||
end
|
||||
|
||||
defp do_delete_at([h|t], index) do
|
||||
[h | do_delete_at(t, index-1)]
|
||||
end
|
||||
|
||||
# zip
|
||||
|
||||
defp do_zip(list, acc) do
|
||||
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
|
||||
{mlist, heads} = :lists.mapfoldl converter, [], list
|
||||
|
||||
case heads do
|
||||
nil -> :lists.reverse acc
|
||||
_ -> do_zip mlist, [list_to_tuple(:lists.reverse(heads))|acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_each(_, nil) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp do_zip_each([h|t], acc) do
|
||||
{t, [h|acc]}
|
||||
end
|
||||
|
||||
defp do_zip_each([], _) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp to_list(tuple) when is_tuple(tuple), do: tuple_to_list(tuple)
|
||||
defp to_list(list) when is_list(list), do: list
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
defprotocol List.Chars do
|
||||
@moduledoc ~S"""
|
||||
The List.Chars protocol is responsible for
|
||||
converting a structure to a list (only if applicable).
|
||||
The only function required to be implemented is
|
||||
`to_char_list` which does the conversion.
|
||||
|
||||
The `to_char_list` function automatically imported
|
||||
by Kernel invokes this protocol.
|
||||
"""
|
||||
|
||||
def to_char_list(thing)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
def to_char_list(atom), do: atom_to_list(atom)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: BitString do
|
||||
@doc """
|
||||
Returns the given binary converted to a char list.
|
||||
"""
|
||||
def to_char_list(thing) when is_binary(thing) do
|
||||
List.from_char_data!(thing)
|
||||
end
|
||||
|
||||
def to_char_list(thing) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: thing,
|
||||
description: "cannot convert a bitstring to a char list"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: List do
|
||||
def to_char_list(list), do: list
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Integer do
|
||||
def to_char_list(thing) do
|
||||
integer_to_list(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Float do
|
||||
@digits 20
|
||||
@limit :math.pow(10, @digits)
|
||||
|
||||
def to_char_list(thing) when thing > @limit do
|
||||
float_to_list(thing, scientific: @digits)
|
||||
end
|
||||
|
||||
def to_char_list(thing) do
|
||||
float_to_list(thing, compact: true, decimals: @digits)
|
||||
end
|
||||
end
|
||||
@@ -1,799 +0,0 @@
|
||||
import Kernel, except: [to_string: 1]
|
||||
|
||||
defmodule Macro do
|
||||
@moduledoc """
|
||||
Conveniences for working with macros.
|
||||
"""
|
||||
|
||||
@typedoc "Abstract Syntax Tree (AST)"
|
||||
@type t :: expr | {t, t} | atom | number | binary | pid | fun | [t]
|
||||
|
||||
@typedoc "Expr node (remaining ones are literals)"
|
||||
@type expr :: {expr | atom, Keyword.t, atom | [t]}
|
||||
|
||||
@binary_ops [:===, :!==,
|
||||
:==, :!=, :<=, :>=,
|
||||
:&&, :||, :<>, :++, :--, :\\, :::, :<-, :.., :|>, :=~,
|
||||
:<, :>, :->,
|
||||
:+, :-, :*, :/, :=, :|, :.,
|
||||
:and, :or, :xor, :when, :in, :inlist, :inbits,
|
||||
:<<<, :>>>, :|||, :&&&, :^^^, :~~~]
|
||||
|
||||
@doc false
|
||||
defmacro binary_ops, do: @binary_ops
|
||||
|
||||
@unary_ops [:!, :@, :^, :not, :+, :-, :~~~, :&]
|
||||
|
||||
@doc false
|
||||
defmacro unary_ops, do: @unary_ops
|
||||
|
||||
@spec binary_op_props(atom) :: {:left | :right, precedence :: integer}
|
||||
defp binary_op_props(o) do
|
||||
case o do
|
||||
o when o in [:<-, :inlist, :inbits, :\\, :::] -> {:left, 40}
|
||||
:| -> {:right, 50}
|
||||
:when -> {:right, 70}
|
||||
:= -> {:right, 80}
|
||||
o when o in [:||, :|||, :or, :xor] -> {:left, 130}
|
||||
o when o in [:&&, :&&&, :and] -> {:left, 140}
|
||||
o when o in [:==, :!=, :<, :<=, :>=, :>, :=~, :===, :!==] -> {:left, 150}
|
||||
o when o in [:|>, :<<<, :>>>] -> {:right, 160}
|
||||
:in -> {:left, 170}
|
||||
o when o in [:++, :--, :.., :<>] -> {:right, 200}
|
||||
o when o in [:+, :-] -> {:left, 210}
|
||||
o when o in [:*, :/] -> {:left, 220}
|
||||
:^^^ -> {:left, 250}
|
||||
:. -> {:left, 310}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Breaks a pipeline expression into a list.
|
||||
|
||||
Raises if the pipeline is ill-formed.
|
||||
"""
|
||||
@spec unpipe(Macro.t) :: [Macro.t]
|
||||
def unpipe({:|> , _, [left, right]}) do
|
||||
[{left, 0}|unpipe(right)]
|
||||
end
|
||||
|
||||
def unpipe(other) do
|
||||
[{other, 0}]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Pipes `expr` into the `call_args` at the given `position`.
|
||||
"""
|
||||
@spec pipe(Macro.t, Macro.t, integer) :: Macro.t | no_return
|
||||
def pipe(expr, call_args, position)
|
||||
|
||||
def pipe(expr, {:&, _, _} = call_args, _integer) do
|
||||
raise ArgumentError, message: "cannot pipe #{to_string expr} into #{to_string call_args}"
|
||||
end
|
||||
|
||||
def pipe(expr, {call, line, atom}, integer) when is_atom(atom) do
|
||||
{call, line, List.insert_at([], integer, expr)}
|
||||
end
|
||||
|
||||
def pipe(expr, {call, line, args}, integer) when is_list(args) do
|
||||
{call, line, List.insert_at(args, integer, expr)}
|
||||
end
|
||||
|
||||
def pipe(expr, call_args, _integer) do
|
||||
raise ArgumentError,
|
||||
message: "cannot pipe #{to_string expr} into #{to_string call_args}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recurs the quoted expression applying the given function to
|
||||
each metadata node.
|
||||
|
||||
This is often useful to remove information like lines and
|
||||
hygienic counters from the expression for either storage or
|
||||
comparison.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> quoted = quote line: 10, do: sample()
|
||||
{:sample, [line: 10], []}
|
||||
iex> Macro.update_meta(quoted, &Keyword.delete(&1, :line))
|
||||
{:sample, [], []}
|
||||
|
||||
"""
|
||||
@spec update_meta(t, (Keyword.t -> Keyword.t)) :: t
|
||||
def update_meta(quoted, fun)
|
||||
|
||||
def update_meta({left, meta, right}, fun) when is_list(meta) do
|
||||
{update_meta(left, fun), fun.(meta), update_meta(right, fun)}
|
||||
end
|
||||
|
||||
def update_meta({left, right}, fun) do
|
||||
{update_meta(left, fun), update_meta(right, fun)}
|
||||
end
|
||||
|
||||
def update_meta(list, fun) when is_list(list) do
|
||||
for x <- list, do: update_meta(x, fun)
|
||||
end
|
||||
|
||||
def update_meta(other, _fun) do
|
||||
other
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decomposes a local or remote call into its remote part (when provided),
|
||||
function name and argument list.
|
||||
|
||||
Returns `:error` when an invalid call syntax is provided.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.decompose_call(quote do: foo)
|
||||
{:foo, []}
|
||||
|
||||
iex> Macro.decompose_call(quote do: foo())
|
||||
{:foo, []}
|
||||
|
||||
iex> Macro.decompose_call(quote do: foo(1, 2, 3))
|
||||
{:foo, [1, 2, 3]}
|
||||
|
||||
iex> Macro.decompose_call(quote do: Elixir.M.foo(1, 2, 3))
|
||||
{{:__aliases__, [], [:Elixir, :M]}, :foo, [1, 2, 3]}
|
||||
|
||||
iex> Macro.decompose_call(quote do: 42)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec decompose_call(Macro.t) :: {atom, [Macro.t]} | {Macro.t, atom, [Macro.t]} | :error
|
||||
def decompose_call({{:., _, [remote, function]}, _, args}) when is_tuple(remote) or is_atom(remote),
|
||||
do: {remote, function, args}
|
||||
|
||||
def decompose_call({name, _, args}) when is_atom(name) and is_atom(args),
|
||||
do: {name, []}
|
||||
|
||||
def decompose_call({name, _, args}) when is_atom(name) and is_list(args),
|
||||
do: {name, args}
|
||||
|
||||
def decompose_call(_),
|
||||
do: :error
|
||||
|
||||
@doc """
|
||||
Recursively escapes a value so it can be inserted
|
||||
into a syntax tree.
|
||||
|
||||
One may pass `unquote: true` to `escape/2`
|
||||
which leaves `unquote` statements unescaped, effectively
|
||||
unquoting the contents on escape.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.escape(:foo)
|
||||
:foo
|
||||
|
||||
iex> Macro.escape({:a, :b, :c})
|
||||
{:{}, [], [:a, :b, :c]}
|
||||
|
||||
iex> Macro.escape({:unquote, [], [1]}, unquote: true)
|
||||
1
|
||||
|
||||
"""
|
||||
@spec escape(term) :: Macro.t
|
||||
@spec escape(term, Keyword.t) :: Macro.t
|
||||
def escape(expr, opts \\ []) do
|
||||
elem(:elixir_quote.escape(expr, Keyword.get(opts, :unquote, false)), 0)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Unescape the given chars.
|
||||
|
||||
This is the unescaping behaviour used by default in Elixir
|
||||
single- and double-quoted strings. Check `unescape_string/2`
|
||||
for information on how to customize the escaping map.
|
||||
|
||||
In this setup, Elixir will escape the following: `\a`, `\b`,
|
||||
`\d`, `\e`, `\f`, `\n`, `\r`, `\s`, `\t` and `\v`. Octals are
|
||||
also escaped according to the latin1 set they represent.
|
||||
|
||||
This function is commonly used on sigil implementations
|
||||
(like `~r`, `~s` and others) which receive a raw, unescaped
|
||||
string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.unescape_string("example\\n")
|
||||
"example\n"
|
||||
|
||||
In the example above, we pass a string with `\n` escaped
|
||||
and return a version with it unescaped.
|
||||
"""
|
||||
@spec unescape_string(String.t) :: String.t
|
||||
def unescape_string(chars) do
|
||||
:elixir_interpolation.unescape_chars(chars)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Unescape the given chars according to the map given.
|
||||
|
||||
Check `unescape_string/1` if you want to use the same map
|
||||
as Elixir single- and double-quoted strings.
|
||||
|
||||
## Map
|
||||
|
||||
The map must be a function. The function receives an integer
|
||||
representing the codepoint of the character it wants to unescape.
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(?b), do: ?\b
|
||||
def unescape_map(?d), do: ?\d
|
||||
def unescape_map(?e), do: ?\e
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?s), do: ?\s
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(e), do: e
|
||||
|
||||
If the `unescape_map` function returns `false`. The char is
|
||||
not escaped and `\` is kept in the char list.
|
||||
|
||||
## Octals
|
||||
|
||||
Octals will by default be escaped unless the map function
|
||||
returns `false` for `?0`.
|
||||
|
||||
## Hex
|
||||
|
||||
Hexadecimals will by default be escaped unless the map function
|
||||
returns `false` for `?x`.
|
||||
|
||||
## Examples
|
||||
|
||||
Using the `unescape_map` function defined above is easy:
|
||||
|
||||
Macro.unescape_string "example\\n", &unescape_map(&1)
|
||||
|
||||
"""
|
||||
@spec unescape_string(String.t, (non_neg_integer -> non_neg_integer | false)) :: String.t
|
||||
def unescape_string(chars, map) do
|
||||
:elixir_interpolation.unescape_chars(chars, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unescape the given tokens according to the default map.
|
||||
|
||||
Check `unescape_string/1` and `unescape_string/2` for more
|
||||
information about unescaping.
|
||||
|
||||
Only tokens that are binaries are unescaped, all others are
|
||||
ignored. This function is useful when implementing your own
|
||||
sigils. Check the implementation of `Kernel.sigil_s/2`
|
||||
for examples.
|
||||
"""
|
||||
@spec unescape_tokens([Macro.t]) :: [Macro.t]
|
||||
def unescape_tokens(tokens) do
|
||||
:elixir_interpolation.unescape_tokens(tokens)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unescape the given tokens according to the given map.
|
||||
|
||||
Check `unescape_tokens/1` and `unescape_string/2` for more information.
|
||||
"""
|
||||
@spec unescape_tokens([Macro.t], (non_neg_integer -> non_neg_integer | false)) :: [Macro.t]
|
||||
def unescape_tokens(tokens, map) do
|
||||
:elixir_interpolation.unescape_tokens(tokens, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given expression to a binary.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.to_string(quote do: foo.bar(1, 2, 3))
|
||||
"foo.bar(1, 2, 3)"
|
||||
|
||||
"""
|
||||
@spec to_string(Macro.t) :: String.t
|
||||
@spec to_string(Macro.t, (Macro.t, String.t -> String.t)) :: String.t
|
||||
def to_string(tree, fun \\ fn(_ast, string) -> string end)
|
||||
|
||||
# Variables
|
||||
def to_string({var, _, atom} = ast, fun) when is_atom(atom) do
|
||||
fun.(ast, atom_to_binary(var))
|
||||
end
|
||||
|
||||
# Aliases
|
||||
def to_string({:__aliases__, _, refs} = ast, fun) do
|
||||
fun.(ast, Enum.map_join(refs, ".", &call_to_string(&1, fun)))
|
||||
end
|
||||
|
||||
# Blocks
|
||||
def to_string({:__block__, _, [expr]} = ast, fun) do
|
||||
fun.(ast, to_string(expr, fun))
|
||||
end
|
||||
|
||||
def to_string({:__block__, _, _} = ast, fun) do
|
||||
block = adjust_new_lines block_to_string(ast, fun), "\n "
|
||||
fun.(ast, "(\n " <> block <> "\n)")
|
||||
end
|
||||
|
||||
# Bits containers
|
||||
def to_string({:<<>>, _, args} = ast, fun) do
|
||||
fun.(ast, case Enum.map_join(args, ", ", &to_string(&1, fun)) do
|
||||
"<" <> rest -> "<< <" <> rest <> " >>"
|
||||
rest -> "<<" <> rest <> ">>"
|
||||
end)
|
||||
end
|
||||
|
||||
# Tuple containers
|
||||
def to_string({:{}, _, args} = ast, fun) do
|
||||
tuple = "{" <> Enum.map_join(args, ", ", &to_string(&1, fun)) <> "}"
|
||||
fun.(ast, tuple)
|
||||
end
|
||||
|
||||
# Map containers
|
||||
def to_string({:%{}, _, args} = ast, fun) do
|
||||
map = "%{" <> map_to_string(args, fun) <> "}"
|
||||
fun.(ast, map)
|
||||
end
|
||||
|
||||
def to_string({:%, _, [structname, map]} = ast, fun) do
|
||||
{:%{}, _, args} = map
|
||||
struct = "%" <> to_string(structname, fun) <> "{" <> map_to_string(args, fun) <> "}"
|
||||
fun.(ast, struct)
|
||||
end
|
||||
|
||||
# Fn keyword
|
||||
def to_string({:fn, _, [{:->, _, [_, tuple]}] = arrow} = ast, fun)
|
||||
when not is_tuple(tuple) or elem(tuple, 0) != :__block__ do
|
||||
fun.(ast, "fn " <> arrow_to_string(arrow, fun) <> " end")
|
||||
end
|
||||
|
||||
def to_string({:fn, _, [{:->, _, _}] = block} = ast, fun) do
|
||||
fun.(ast, "fn " <> block_to_string(block, fun) <> "\nend")
|
||||
end
|
||||
|
||||
def to_string({:fn, _, block} = ast, fun) do
|
||||
block = adjust_new_lines block_to_string(block, fun), "\n "
|
||||
fun.(ast, "fn\n " <> block <> "\nend")
|
||||
end
|
||||
|
||||
# left -> right
|
||||
def to_string([{:->, _, _}|_] = ast, fun) do
|
||||
fun.(ast, "(" <> arrow_to_string(ast, fun, true) <> ")")
|
||||
end
|
||||
|
||||
# left when right
|
||||
def to_string({:when, _, [left, right]} = ast, fun) do
|
||||
if right != [] and Keyword.keyword?(right) do
|
||||
right = kw_list_to_string(right, fun)
|
||||
else
|
||||
right = fun.(ast, op_to_string(right, fun, :when, :right))
|
||||
end
|
||||
|
||||
fun.(ast, op_to_string(left, fun, :when, :left) <> " when " <> right)
|
||||
end
|
||||
|
||||
# Binary ops
|
||||
def to_string({op, _, [left, right]} = ast, fun) when op in unquote(@binary_ops) do
|
||||
fun.(ast, op_to_string(left, fun, op, :left) <> " #{op} " <> op_to_string(right, fun, op, :right))
|
||||
end
|
||||
|
||||
# Splat when
|
||||
def to_string({:when, _, args} = ast, fun) do
|
||||
{left, right} = :elixir_utils.split_last(args)
|
||||
fun.(ast, "(" <> Enum.map_join(left, ", ", &to_string(&1, fun)) <> ") when " <> to_string(right, fun))
|
||||
end
|
||||
|
||||
# Unary ops
|
||||
def to_string({unary, _, [{binary, _, [_, _]} = arg]} = ast, fun)
|
||||
when unary in unquote(@unary_ops) and binary in unquote(@binary_ops) do
|
||||
fun.(ast, atom_to_binary(unary) <> "(" <> to_string(arg, fun) <> ")")
|
||||
end
|
||||
|
||||
def to_string({:not, _, [arg]} = ast, fun) do
|
||||
fun.(ast, "not " <> to_string(arg, fun))
|
||||
end
|
||||
|
||||
def to_string({op, _, [arg]} = ast, fun) when op in unquote(@unary_ops) do
|
||||
fun.(ast, atom_to_binary(op) <> to_string(arg, fun))
|
||||
end
|
||||
|
||||
# Access
|
||||
def to_string({{:., _, [Kernel, :access]}, _, [left, right]} = ast, fun) do
|
||||
fun.(ast, to_string(left, fun) <> to_string(right, fun))
|
||||
end
|
||||
|
||||
# All other calls
|
||||
def to_string({target, _, args} = ast, fun) when is_list(args) do
|
||||
{list, last} = :elixir_utils.split_last(args)
|
||||
fun.(ast, case kw_blocks?(last) do
|
||||
true -> call_to_string_with_args(target, list, fun) <> kw_blocks_to_string(last, fun)
|
||||
false -> call_to_string_with_args(target, args, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
# Two-item tuples
|
||||
def to_string({left, right}, fun) do
|
||||
to_string({:{}, [], [left, right]}, fun)
|
||||
end
|
||||
|
||||
# Lists
|
||||
def to_string(list, fun) when is_list(list) do
|
||||
fun.(list, cond do
|
||||
list == [] ->
|
||||
"[]"
|
||||
:io_lib.printable_list(list) ->
|
||||
"'" <> Inspect.BitString.escape(String.from_char_data!(list), ?') <> "'"
|
||||
Keyword.keyword?(list) ->
|
||||
"[" <> kw_list_to_string(list, fun) <> "]"
|
||||
true ->
|
||||
"[" <> Enum.map_join(list, ", ", &to_string(&1, fun)) <> "]"
|
||||
end)
|
||||
end
|
||||
|
||||
# All other structures
|
||||
def to_string(other, fun), do: fun.(other, inspect(other, records: false))
|
||||
|
||||
# Block keywords
|
||||
@kw_keywords [:do, :catch, :rescue, :after, :else]
|
||||
|
||||
defp kw_blocks?([_|_] = kw) do
|
||||
Enum.all?(kw, &match?({x, _} when x in unquote(@kw_keywords), &1))
|
||||
end
|
||||
defp kw_blocks?(_), do: false
|
||||
|
||||
defp module_to_string(atom, _fun) when is_atom(atom), do: inspect(atom, records: false)
|
||||
defp module_to_string(other, fun), do: call_to_string(other, fun)
|
||||
|
||||
defp call_to_string(atom, _fun) when is_atom(atom), do: atom_to_binary(atom)
|
||||
defp call_to_string({:., _, [arg]}, fun), do: module_to_string(arg, fun) <> "."
|
||||
defp call_to_string({:., _, [left, right]}, fun), do: module_to_string(left, fun) <> "." <> call_to_string(right, fun)
|
||||
defp call_to_string(other, fun), do: to_string(other, fun)
|
||||
|
||||
defp call_to_string_with_args(target, args, fun) do
|
||||
target = call_to_string(target, fun)
|
||||
args = args_to_string(args, fun)
|
||||
target <> "(" <> args <> ")"
|
||||
end
|
||||
|
||||
defp args_to_string(args, fun) do
|
||||
{list, last} = :elixir_utils.split_last(args)
|
||||
|
||||
if last != [] and Keyword.keyword?(last) do
|
||||
args = Enum.map_join(list, ", ", &to_string(&1, fun))
|
||||
if list != [], do: args = args <> ", "
|
||||
args <> kw_list_to_string(last, fun)
|
||||
else
|
||||
Enum.map_join(args, ", ", &to_string(&1, fun))
|
||||
end
|
||||
end
|
||||
|
||||
defp kw_blocks_to_string(kw, fun) do
|
||||
Enum.reduce(@kw_keywords, " ", fn(x, acc) ->
|
||||
case Keyword.has_key?(kw, x) do
|
||||
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
|
||||
false -> acc
|
||||
end
|
||||
end) <> "end"
|
||||
end
|
||||
|
||||
defp kw_block_to_string(key, value, fun) do
|
||||
block = adjust_new_lines block_to_string(value, fun), "\n "
|
||||
atom_to_binary(key) <> "\n " <> block <> "\n"
|
||||
end
|
||||
|
||||
defp block_to_string([{:->, _, _}|_] = block, fun) do
|
||||
Enum.map_join(block, "\n", fn({:->, _, [left, right]}) ->
|
||||
left = comma_join_or_empty_paren(left, fun, false)
|
||||
left <> "->\n " <> adjust_new_lines block_to_string(right, fun), "\n "
|
||||
end)
|
||||
end
|
||||
|
||||
defp block_to_string({:__block__, _, exprs}, fun) do
|
||||
Enum.map_join(exprs, "\n", &to_string(&1, fun))
|
||||
end
|
||||
|
||||
defp block_to_string(other, fun), do: to_string(other, fun)
|
||||
|
||||
defp map_to_string([{:|, _, [update_map, update_args]}], fun) do
|
||||
to_string(update_map, fun) <> " | " <> map_to_string(update_args, fun)
|
||||
end
|
||||
|
||||
defp map_to_string(list, fun) do
|
||||
cond do
|
||||
Keyword.keyword?(list) -> kw_list_to_string(list, fun)
|
||||
true -> map_list_to_string(list, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp kw_list_to_string(list, fun) do
|
||||
Enum.map_join(list, ", ", fn {key, value} ->
|
||||
atom_to_binary(key) <> ": " <> to_string(value, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_list_to_string(list, fun) do
|
||||
Enum.map_join(list, ", ", fn {key, value} ->
|
||||
to_string(key, fun) <> " => " <> to_string(value, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp parenthise(expr, fun) do
|
||||
"(" <> to_string(expr, fun) <> ")"
|
||||
end
|
||||
|
||||
defp op_to_string({op, _, [_, _]} = expr, fun, parent_op, side) when op in unquote(@binary_ops) do
|
||||
{parent_assoc, parent_prec} = binary_op_props(parent_op)
|
||||
{_, prec} = binary_op_props(op)
|
||||
cond do
|
||||
parent_prec < prec -> to_string(expr, fun)
|
||||
parent_prec > prec -> parenthise(expr, fun)
|
||||
true ->
|
||||
# parent_prec == prec, so look at associativity.
|
||||
if parent_assoc == side do
|
||||
to_string(expr, fun)
|
||||
else
|
||||
parenthise(expr, fun)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp op_to_string(expr, fun, _, _), do: to_string(expr, fun)
|
||||
|
||||
defp arrow_to_string(pairs, fun, paren \\ false) do
|
||||
Enum.map_join(pairs, "; ", fn({:->, _, [left, right]}) ->
|
||||
left = comma_join_or_empty_paren(left, fun, paren)
|
||||
left <> "-> " <> to_string(right, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp comma_join_or_empty_paren([], _fun, true), do: "() "
|
||||
defp comma_join_or_empty_paren([], _fun, false), do: ""
|
||||
|
||||
defp comma_join_or_empty_paren(left, fun, _) do
|
||||
Enum.map_join(left, ", ", &to_string(&1, fun)) <> " "
|
||||
end
|
||||
|
||||
defp adjust_new_lines(block, replacement) do
|
||||
for <<x <- block>>, into: "" do
|
||||
case x == ?\n do
|
||||
true -> replacement
|
||||
false -> <<x>>
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it once.
|
||||
|
||||
The following contents are expanded:
|
||||
|
||||
* Macros (local or remote);
|
||||
* Aliases are expanded (if possible) and return atoms;
|
||||
* Pseudo-variables (`__ENV__`, `__MODULE__` and `__DIR__`);
|
||||
* Module attributes reader (`@foo`);
|
||||
|
||||
If the expression cannot be expanded, it returns the expression
|
||||
itself. Notice that `expand_once/2` performs the expansion just
|
||||
once and it is not recursive. Check `expand/2` for expansion
|
||||
until the node can no longer be expanded.
|
||||
|
||||
## Examples
|
||||
|
||||
In the example below, we have a macro that generates a module
|
||||
with a function named `name_length` that returns the length
|
||||
of the module name. The value of this function will be calculated
|
||||
at compilation time and not at runtime.
|
||||
|
||||
Consider the implementation below:
|
||||
|
||||
defmacro defmodule_with_length(name, do: block) do
|
||||
length = length(atom_to_list(name))
|
||||
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
def name_length, do: unquote(length)
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
When invoked like this:
|
||||
|
||||
defmodule_with_length My.Module do
|
||||
def other_function, do: ...
|
||||
end
|
||||
|
||||
The compilation will fail because `My.Module` when quoted
|
||||
is not an atom, but a syntax tree as follow:
|
||||
|
||||
{:__aliases__, [], [:My, :Module]}
|
||||
|
||||
That said, we need to expand the aliases node above to an
|
||||
atom, so we can retrieve its length. Expanding the node is
|
||||
not straight-forward because we also need to expand the
|
||||
caller aliases. For example:
|
||||
|
||||
alias MyHelpers, as: My
|
||||
|
||||
defmodule_with_length My.Module do
|
||||
def other_function, do: ...
|
||||
end
|
||||
|
||||
The final module name will be `MyHelpers.Module` and not
|
||||
`My.Module`. With `Macro.expand/2`, such aliases are taken
|
||||
into consideration. Local and remote macros are also
|
||||
expanded. We could rewrite our macro above to use this
|
||||
function as:
|
||||
|
||||
defmacro defmodule_with_length(name, do: block) do
|
||||
expanded = Macro.expand(name, __CALLER__)
|
||||
length = length(atom_to_list(expanded))
|
||||
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
def name_length, do: unquote(length)
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
def expand_once(ast, env) do
|
||||
elem(do_expand_once(ast, env), 0)
|
||||
end
|
||||
|
||||
defp do_expand_once({:__aliases__, _, _} = original, env) do
|
||||
case :elixir_aliases.expand(original, env.aliases, env.macro_aliases, env.lexical_tracker) do
|
||||
receiver when is_atom(receiver) ->
|
||||
:elixir_lexical.record_remote(receiver, env.lexical_tracker)
|
||||
{receiver, true}
|
||||
aliases ->
|
||||
aliases = for alias <- aliases, do: elem(do_expand_once(alias, env), 0)
|
||||
|
||||
case :lists.all(&is_atom/1, aliases) do
|
||||
true ->
|
||||
receiver = :elixir_aliases.concat(aliases)
|
||||
:elixir_lexical.record_remote(receiver, env.lexical_tracker)
|
||||
{receiver, true}
|
||||
false ->
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Expand @ calls
|
||||
defp do_expand_once({:@, _, [{name, _, args}]} = original, env) when is_atom(args) or args == [] do
|
||||
case (module = env.module) && Module.open?(module) do
|
||||
true -> {escape(Module.get_attribute(module, name)), true}
|
||||
false -> {original, false}
|
||||
end
|
||||
end
|
||||
|
||||
# Expand pseudo-variables
|
||||
defp do_expand_once({:__MODULE__, _, atom}, env) when is_atom(atom),
|
||||
do: {env.module, true}
|
||||
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
|
||||
do: {:filename.dirname(env.file), true}
|
||||
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom),
|
||||
do: {{:%{}, [], Map.to_list(env)}, true}
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) when
|
||||
is_atom(atom) and is_atom(field) do
|
||||
if Map.has_key?(env, field) do
|
||||
{Map.get(env, field), true}
|
||||
else
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
|
||||
# Expand possible macro import invocation
|
||||
defp do_expand_once({atom, meta, context} = original, env)
|
||||
when is_atom(atom) and is_list(meta) and is_atom(context) do
|
||||
if :lists.member({atom, Keyword.get(meta, :counter, context)}, env.vars) do
|
||||
{original, false}
|
||||
else
|
||||
case do_expand_once({atom, meta, []}, env) do
|
||||
{_, true} = exp -> exp
|
||||
{_, false} -> {original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_expand_once({atom, meta, args} = original, env)
|
||||
when is_atom(atom) and is_list(args) and is_list(meta) do
|
||||
arity = length(args)
|
||||
|
||||
if :elixir_import.special_form(atom, arity) do
|
||||
{original, false}
|
||||
else
|
||||
module = env.module
|
||||
extra = if function_exported?(module, :__info__, 1) do
|
||||
[{module, module.__info__(:macros)}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
expand = :elixir_dispatch.expand_import(meta, {atom, length(args)}, args,
|
||||
env, extra)
|
||||
|
||||
case expand do
|
||||
{:ok, receiver, quoted} ->
|
||||
next = :elixir_counter.next
|
||||
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
|
||||
{:ok, _receiver} ->
|
||||
{original, false}
|
||||
:error ->
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Expand possible macro require invocation
|
||||
defp do_expand_once({{:., _, [left, right]}, meta, args} = original, env) when is_atom(right) do
|
||||
{receiver, _} = do_expand_once(left, env)
|
||||
|
||||
case is_atom(receiver) do
|
||||
false -> {original, false}
|
||||
true ->
|
||||
expand = :elixir_dispatch.expand_require(meta, receiver, {right, length(args)}, args, env)
|
||||
|
||||
case expand do
|
||||
{:ok, receiver, quoted} ->
|
||||
next = :elixir_counter.next
|
||||
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
|
||||
:error ->
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Anything else is just returned
|
||||
defp do_expand_once(other, _env), do: {other, false}
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it until it can no longer
|
||||
be expanded.
|
||||
|
||||
This function uses `expand_once/2` under the hood. Check
|
||||
`expand_once/2` for more information and exmaples.
|
||||
"""
|
||||
def expand(tree, env) do
|
||||
expand_until({tree, true}, env)
|
||||
end
|
||||
|
||||
defp expand_until({tree, true}, env) do
|
||||
expand_until(do_expand_once(tree, env), env)
|
||||
end
|
||||
|
||||
defp expand_until({tree, false}, _env) do
|
||||
tree
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recursively traverses the quoted expression checking if all sub-terms are
|
||||
safe.
|
||||
|
||||
Terms are considered safe if they represent data structures and don't actually
|
||||
evaluate code. Returns `:ok` unless a given term is unsafe,
|
||||
which is returned as `{:unsafe, term}`.
|
||||
"""
|
||||
def safe_term(terms) do
|
||||
IO.write :stderr, "Macro.safe_term/1 is deprecated\n#{Exception.format_stacktrace}"
|
||||
do_safe_term(terms) || :ok
|
||||
end
|
||||
|
||||
defp do_safe_term({local, _, terms}) when local in [:{}, :%{}, :__aliases__] do
|
||||
do_safe_term(terms)
|
||||
end
|
||||
|
||||
defp do_safe_term({unary, _, [term]}) when unary in [:+, :-] do
|
||||
do_safe_term(term)
|
||||
end
|
||||
|
||||
defp do_safe_term({left, right}), do: do_safe_term(left) || do_safe_term(right)
|
||||
defp do_safe_term(terms) when is_list(terms), do: Enum.find_value(terms, &do_safe_term(&1))
|
||||
defp do_safe_term(terms) when is_tuple(terms), do: {:unsafe, terms}
|
||||
defp do_safe_term(_), do: nil
|
||||
end
|
||||
@@ -1,119 +0,0 @@
|
||||
defmodule Macro.Env do
|
||||
@moduledoc """
|
||||
A struct that holds compile time environment information.
|
||||
|
||||
The current environment can be accessed at any time as
|
||||
`__ENV__`. Inside macros, the caller environment can be
|
||||
accessed as `__CALLER__`. It contains the following fields:
|
||||
|
||||
* `module` - the current module name.
|
||||
* `file` - the current file name as a binary
|
||||
* `line` - the current line as an integer
|
||||
* `function` - a tuple as `{atom, integer`}, where the first
|
||||
element is the function name and the seconds its arity. Returns
|
||||
`nil` if not inside a function
|
||||
* `context` - the context of the environment. It can be nil
|
||||
(default context), inside a guard or inside an assign
|
||||
* `aliases` - a list of two item tuples, where the first
|
||||
item is the aliased name and the second the actual name
|
||||
* `requires` - the list of required modules
|
||||
* `functions` - a list of functions imported from each module
|
||||
* `macros` - a list of macros imported from each module
|
||||
* `macro_aliases` - a list of aliases defined inside the current macro
|
||||
* `context_modules` - a list of modules defined in the current context
|
||||
* `vars` - a list keeping all defined variables as {var, context}
|
||||
* `export_vars` - a list keeping all variables to be exported in a construct (may be nil)
|
||||
* `lexical_tracker` - PID to the lexical tracker which is responsible to keep user info
|
||||
* `local` - the module to expand local functions to
|
||||
"""
|
||||
|
||||
@type name_arity :: {atom, non_neg_integer}
|
||||
@type file :: binary
|
||||
@type line :: non_neg_integer
|
||||
@type aliases :: [{module, module}]
|
||||
@type macro_aliases :: [{module, {integer, module}}]
|
||||
@type context :: :match | :guard | nil
|
||||
@type requires :: [module]
|
||||
@type functions :: [{module, [name_arity]}]
|
||||
@type macros :: [{module, [name_arity]}]
|
||||
@type context_modules :: [module]
|
||||
@type vars :: [{atom, atom | non_neg_integer}]
|
||||
@type export_vars :: vars | nil
|
||||
@type lexical_tracker :: pid
|
||||
@type local :: module | nil
|
||||
|
||||
@type t :: %{__struct__: __MODULE__,
|
||||
module: module,
|
||||
file: file,
|
||||
line: line,
|
||||
function: name_arity | nil,
|
||||
context: context,
|
||||
requires: requires,
|
||||
aliases: aliases,
|
||||
functions: functions,
|
||||
macros: macros,
|
||||
macro_aliases: aliases,
|
||||
context_modules: context_modules,
|
||||
vars: vars,
|
||||
export_vars: export_vars,
|
||||
lexical_tracker: lexical_tracker,
|
||||
local: local}
|
||||
|
||||
def __struct__ do
|
||||
%{__struct__: __MODULE__,
|
||||
module: nil,
|
||||
file: "nofile",
|
||||
line: 0,
|
||||
function: nil,
|
||||
context: nil,
|
||||
requires: [],
|
||||
aliases: [],
|
||||
functions: [],
|
||||
macros: [],
|
||||
macro_aliases: [],
|
||||
context_modules: [],
|
||||
vars: [],
|
||||
export_vars: nil,
|
||||
lexical_tracker: nil,
|
||||
local: nil}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a keyword list containing the file and line
|
||||
information as keys.
|
||||
"""
|
||||
def location(%{__struct__: Macro.Env, file: file, line: line}) do
|
||||
[file: file, line: line]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the compilation environment is currently
|
||||
inside a guard.
|
||||
"""
|
||||
def in_guard?(%{__struct__: Macro.Env, context: context}), do: context == :guard
|
||||
|
||||
@doc """
|
||||
Returns whether the compilation environment is currently
|
||||
inside a match clause.
|
||||
"""
|
||||
def in_match?(%{__struct__: Macro.Env, context: context}), do: context == :match
|
||||
|
||||
@doc """
|
||||
Returns the environment stacktrace.
|
||||
"""
|
||||
def stacktrace(%{__struct__: Macro.Env} = env) do
|
||||
cond do
|
||||
nil?(env.module) ->
|
||||
[{:elixir_compiler, :__FILE__, 1, relative_location(env)}]
|
||||
nil?(env.function) ->
|
||||
[{env.module, :__MODULE__, 0, relative_location(env)}]
|
||||
true ->
|
||||
{name, arity} = env.function
|
||||
[{env.module, name, arity, relative_location(env)}]
|
||||
end
|
||||
end
|
||||
|
||||
defp relative_location(env) do
|
||||
[file: Path.relative_to_cwd(env.file), line: env.line]
|
||||
end
|
||||
end
|
||||
@@ -1,82 +0,0 @@
|
||||
defmodule Map do
|
||||
@moduledoc """
|
||||
A Dict implementation that works on maps.
|
||||
|
||||
Maps are key-value stores where keys are compared using
|
||||
the match operator (`===`). Maps can be created with
|
||||
the `%{}` special form defined in the `Kernel.SpecialForms`
|
||||
module.
|
||||
|
||||
For more information about the functions in this module and
|
||||
their APIs, please consult the `Dict` module.
|
||||
"""
|
||||
|
||||
use Dict.Behaviour
|
||||
|
||||
defdelegate [keys(map), values(map), size(map), merge(map1, map2), to_list(map)], to: :maps
|
||||
|
||||
@doc """
|
||||
Returns a new empty map.
|
||||
"""
|
||||
def new, do: %{}
|
||||
|
||||
def has_key?(map, key), do: :maps.is_key(key, map)
|
||||
|
||||
def fetch(map, key), do: :maps.find(key, map)
|
||||
|
||||
def pop(map, key, default \\ nil) do
|
||||
{get(map, key, default), delete(map, key)}
|
||||
end
|
||||
|
||||
def put(map, key, val) do
|
||||
:maps.put(key, val, map)
|
||||
end
|
||||
|
||||
def put_new(map, key, val) do
|
||||
case has_key?(map, key) do
|
||||
true -> map
|
||||
false -> :maps.put(key, val, map)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(map, key), do: :maps.remove(key, map)
|
||||
|
||||
def merge(map1, map2, callback) do
|
||||
:maps.fold fn k, v2, acc ->
|
||||
update(acc, k, v2, fn(v1) -> callback.(k, v1, v2) end)
|
||||
end, map1, map2
|
||||
end
|
||||
|
||||
def split(map, keys) do
|
||||
acc = {%{}, %{}}
|
||||
|
||||
:maps.fold fn k, v, {take, drop} ->
|
||||
if k in keys do
|
||||
{put(take, k, v), drop}
|
||||
else
|
||||
{take, put(drop, k, v)}
|
||||
end
|
||||
end, acc, map
|
||||
end
|
||||
|
||||
def update!(map, key, fun) do
|
||||
case :maps.find(key, map) do
|
||||
:error ->
|
||||
raise(KeyError, key: key, term: map)
|
||||
{:ok, val} ->
|
||||
:maps.put(key, fun.(val), map)
|
||||
end
|
||||
end
|
||||
|
||||
def update(map, key, initial, fun) do
|
||||
case :maps.find(key, map) do
|
||||
:error ->
|
||||
:maps.put(key, initial, map)
|
||||
{:ok, val} ->
|
||||
:maps.put(key, fun.(val), map)
|
||||
end
|
||||
end
|
||||
|
||||
def equal?(map, map), do: true
|
||||
def equal?(_, _), do: false
|
||||
end
|
||||
@@ -1,975 +0,0 @@
|
||||
defmodule Module do
|
||||
@moduledoc ~S'''
|
||||
This module provides many functions to deal with modules during
|
||||
compilation time. It allows a developer to dynamically attach
|
||||
documentation, add, delete and register attributes and so forth.
|
||||
|
||||
After a module is compiled, using many of the functions in
|
||||
this module will raise errors, since it is out of their scope
|
||||
to inspect runtime data. Most of the runtime data can be inspected
|
||||
via the `__info__(attr)` function attached to each compiled module.
|
||||
|
||||
## Module attributes
|
||||
|
||||
Each module can be decorated with one or more attributes. The following ones
|
||||
are currently defined by Elixir:
|
||||
|
||||
* `@after_compile`
|
||||
|
||||
A hook that will be invoked right after the current module is compiled.
|
||||
|
||||
Accepts a module or a tuple `{<module>, <function atom>}`. The function
|
||||
must take two arguments: the module environment and its bytecode.
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__after_compile__/2`.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@after_compile __MODULE__
|
||||
|
||||
def __after_compile__(env, _bytecode) do
|
||||
IO.inspect env
|
||||
end
|
||||
end
|
||||
|
||||
* `@before_compile`
|
||||
|
||||
A hook that will be invoked before the module is compiled.
|
||||
|
||||
Accepts a module or a tuple `{<module>, <function/macro atom>}`. The
|
||||
function/macro must take one argument: the module environment. If it's a
|
||||
macro, its returned value will be injected at the end of the module definition
|
||||
before the compilation starts.
|
||||
|
||||
When just a module is provided, the function/macro is assumed to be
|
||||
`__before_compile__/1`.
|
||||
|
||||
Note: unlike `@after_compile`, the callback function/macro must
|
||||
be placed in a separate module (because when the callback is invoked,
|
||||
the current module does not yet exist).
|
||||
|
||||
### Example
|
||||
|
||||
defmodule A do
|
||||
defmacro __before_compile__(_env) do
|
||||
quote do
|
||||
def hello, do: "world"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule B do
|
||||
@before_compile A
|
||||
end
|
||||
|
||||
* `@behaviour` (notice the British spelling)
|
||||
|
||||
Specify an OTP or user-defined behaviour.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@behaviour gen_event
|
||||
|
||||
# ...
|
||||
end
|
||||
|
||||
* `@compile`
|
||||
|
||||
Define options for module compilation that are passed to the Erlang
|
||||
compiler.
|
||||
|
||||
Accepts an atom, a tuple, or a list of atoms and tuples.
|
||||
|
||||
See http://www.erlang.org/doc/man/compile.html for the list of supported
|
||||
options.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@compile {:inline, myfun: 1}
|
||||
|
||||
def myfun(arg) do
|
||||
to_string(arg)
|
||||
end
|
||||
end
|
||||
|
||||
* `@doc`
|
||||
|
||||
Provide documentation for the function or macro that follows the
|
||||
attribute.
|
||||
|
||||
Accepts a string (often a heredoc) or `false` where `@doc false` will
|
||||
make the function/macro invisible to the documentation extraction tools
|
||||
like ExDoc.
|
||||
|
||||
Can be invoked more than once.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@doc "Hello world"
|
||||
def hello do
|
||||
"world"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sum.
|
||||
"""
|
||||
def sum(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
|
||||
* `@file`
|
||||
|
||||
Change the filename used in stacktraces for the function or macro that
|
||||
follows the attribute.
|
||||
|
||||
Accepts a string. Can be used more than once.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@doc "Hello world"
|
||||
@file "hello.ex"
|
||||
def hello do
|
||||
"world"
|
||||
end
|
||||
end
|
||||
|
||||
* `@moduledoc`
|
||||
|
||||
Provide documentation for the current module.
|
||||
|
||||
Accepts a string (which is often a heredoc) or `false` where
|
||||
`@moduledoc false` will make the module invisible to the
|
||||
documentation extraction tools like ExDoc.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@moduledoc """
|
||||
A very useful module
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
* `@on_definition`
|
||||
|
||||
A hook that will be invoked when each function or macro in the current
|
||||
module is defined. Useful when annotating functions.
|
||||
|
||||
Accepts a module or a tuple `{<module>, <function atom>}`. The function
|
||||
must take 6 arguments:
|
||||
|
||||
- the module environment
|
||||
- kind: `:def`, `:defp`, `:defmacro`, or `:defmacrop`
|
||||
- function/macro name
|
||||
- list of expanded arguments
|
||||
- list of expanded guards
|
||||
- expanded function body
|
||||
|
||||
Note the hook receives the expanded arguments and it is invoked before
|
||||
the function is stored in the module. So `Module.defines?/2` will return
|
||||
false for the first clause of every function.
|
||||
|
||||
If the function/macro being defined has multiple clauses, the hook will
|
||||
be called for each clause.
|
||||
|
||||
Unlike other hooks, `@on_definition` will only invoke functions
|
||||
and never macros. This is because the hook is invoked inside the context
|
||||
of the function (and nested function definitions are not allowed in
|
||||
Elixir).
|
||||
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__on_definition__/6`.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule H do
|
||||
def on_def(_env, kind, name, args, guards, body) do
|
||||
IO.puts "Defining #{kind} named #{name} with args:"
|
||||
IO.inspect args
|
||||
IO.puts "and guards"
|
||||
IO.inspect guards
|
||||
IO.puts "and body"
|
||||
IO.puts Macro.to_string(body)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule M do
|
||||
@on_definition {H, :on_def}
|
||||
|
||||
def hello(arg) when is_binary(arg) or is_list(arg) do
|
||||
"Hello" <> to_string(arg)
|
||||
end
|
||||
|
||||
def hello(_) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
* `@on_load`
|
||||
|
||||
A hook that will be invoked whenever the module is loaded.
|
||||
|
||||
Accepts a function atom of a function in the current module. The function
|
||||
must have arity 0 (no arguments) and has to return `:ok`, otherwise the
|
||||
loading of the module will be aborted.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@on_load :load_check
|
||||
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
* `@vsn`
|
||||
|
||||
Specify the module version. Accepts any valid Elixir value.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@vsn "1.0"
|
||||
end
|
||||
|
||||
The following attributes are part of typespecs and are also reserved by
|
||||
Elixir (see `Kernel.Typespec` for more information about typespecs):
|
||||
|
||||
* `@type` - defines a type to be used in `@spec`
|
||||
* `@typep` - defines a private type to be used in `@spec`
|
||||
* `@opaque` - defines an opaque type to be used in `@spec`
|
||||
* `@spec` - provides a specification for a function
|
||||
* `@callback` - provides a specification for the behaviour callback
|
||||
|
||||
In addition to the built-in attributes outlined above, custom attributes may
|
||||
also be added. A custom attribute is any valid identifier prefixed with an
|
||||
`@` and followed by a valid Elixir value:
|
||||
|
||||
defmodule M do
|
||||
@custom_attr [some: "stuff"]
|
||||
end
|
||||
|
||||
For more advanced options available when defining custom attributes, see
|
||||
`register_attribute/3`.
|
||||
|
||||
## Runtime information about a module
|
||||
|
||||
It is possible to query a module at runtime to find out which functions and
|
||||
macros it defines, extract its docstrings, etc. See `__info__/1`.
|
||||
'''
|
||||
|
||||
@doc """
|
||||
Provides runtime information about functions and macros defined by the
|
||||
module, enables docstring extraction, etc.
|
||||
|
||||
Each module gets an `__info__/1` function when it's compiled. The function
|
||||
takes one of the following atoms:
|
||||
|
||||
* `:functions` - keyword list of public functions along with their arities
|
||||
|
||||
* `:macros` - keyword list of public macros along with their arities
|
||||
|
||||
* `:docs` - list of all docstrings attached to functions and macros
|
||||
using the `@doc` attribute
|
||||
|
||||
* `:moduledoc` - tuple `{<line>, <doc>}` where `line` is the line on
|
||||
which module definition starts and `doc` is the string
|
||||
attached to the module using the `@moduledoc` attribute
|
||||
|
||||
* `:module` - module name (`Module == Module.__info__(:module)`)
|
||||
|
||||
In addition to the above, you may also pass to `__info__/1` any atom supported
|
||||
by Erlang's `module_info` function which also gets defined for each compiled
|
||||
module. See http://erlang.org/doc/reference_manual/modules.html#id69430 for
|
||||
more information.
|
||||
"""
|
||||
def __info__(kind)
|
||||
|
||||
@doc """
|
||||
Check if a module is open, i.e. it is currently being defined
|
||||
and its attributes and functions can be modified.
|
||||
"""
|
||||
def open?(module) do
|
||||
:elixir_module.is_open(module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluates the quoted contents in the given module's context.
|
||||
|
||||
A list of environment options can also be given as argument.
|
||||
See `Code.eval_string/3` for more information.
|
||||
|
||||
Raises an error if the module was already compiled.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
contents = quote do: (def sum(a, b), do: a + b)
|
||||
Module.eval_quoted __MODULE__, contents
|
||||
end
|
||||
|
||||
Foo.sum(1, 2) #=> 3
|
||||
|
||||
For convenience, you can my pass `__ENV__` as argument and
|
||||
all options will be automatically extracted from the environment:
|
||||
|
||||
defmodule Foo do
|
||||
contents = quote do: (def sum(a, b), do: a + b)
|
||||
Module.eval_quoted __MODULE__, contents, [], __ENV__
|
||||
end
|
||||
|
||||
Foo.sum(1, 2) #=> 3
|
||||
|
||||
"""
|
||||
def eval_quoted(module, quoted, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts) do
|
||||
eval_quoted(env.module, quoted, binding, Keyword.merge(Map.to_list(env), opts))
|
||||
end
|
||||
|
||||
def eval_quoted(module, quoted, binding, %Macro.Env{} = env) do
|
||||
eval_quoted(module, quoted, binding, Map.to_list(env))
|
||||
end
|
||||
|
||||
def eval_quoted(module, quoted, binding, opts) do
|
||||
assert_not_compiled!(:eval_quoted, module)
|
||||
:elixir_def.reset_last(module)
|
||||
{value, binding, _env, _scope} =
|
||||
:elixir.eval_quoted quoted, binding, Keyword.put(opts, :module, module)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a module with the given name and defined by
|
||||
the given quoted expressions. The line where the module
|
||||
is defined and its file can be passed as options.
|
||||
|
||||
## Examples
|
||||
|
||||
contents =
|
||||
quote do
|
||||
def world, do: true
|
||||
end
|
||||
|
||||
Module.create(Hello, contents, Macro.Env.location(__ENV__))
|
||||
|
||||
Hello.world #=> true
|
||||
|
||||
## Differences from `defmodule`
|
||||
|
||||
`Module.create` works similarly to `defmodule` and
|
||||
return the same results. While one could also use
|
||||
`defmodule` to define modules dynamically, this
|
||||
function is preferred when the module body is given
|
||||
by a quoted expression.
|
||||
|
||||
Another important distinction is that `Module.create`
|
||||
allows you to control the environment variables used
|
||||
when defining the module, while `defmodule` automatically
|
||||
shares the same environment.
|
||||
"""
|
||||
def create(module, quoted, opts \\ [])
|
||||
|
||||
def create(module, quoted, %Macro.Env{} = env) do
|
||||
create(module, quoted, Map.to_list(env))
|
||||
end
|
||||
|
||||
def create(module, quoted, opts) when is_atom(module) do
|
||||
:elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a list of aliases and returns a new alias.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.concat([Foo, Bar])
|
||||
Foo.Bar
|
||||
|
||||
iex> Module.concat([Foo, "Bar"])
|
||||
Foo.Bar
|
||||
|
||||
"""
|
||||
@spec concat([binary | atom]) :: atom
|
||||
def concat(list) when is_list(list) do
|
||||
:elixir_aliases.concat(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two aliases and returns a new alias.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.concat(Foo, Bar)
|
||||
Foo.Bar
|
||||
|
||||
iex> Module.concat(Foo, "Bar")
|
||||
Foo.Bar
|
||||
|
||||
"""
|
||||
@spec concat(binary | atom, binary | atom) :: atom
|
||||
def concat(left, right) do
|
||||
:elixir_aliases.concat([left, right])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a list of aliases and returns a new alias only
|
||||
if the alias was already referenced. If the alias was not
|
||||
referenced yet, fails with `ArgumentError`.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.safe_concat([Unknown, Module])
|
||||
** (ArgumentError) argument error
|
||||
|
||||
iex> Module.safe_concat([List, Chars])
|
||||
List.Chars
|
||||
|
||||
"""
|
||||
@spec safe_concat([binary | atom]) :: atom | no_return
|
||||
def safe_concat(list) when is_list(list) do
|
||||
:elixir_aliases.safe_concat(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two aliases and returns a new alias only
|
||||
if the alias was already referenced. If the alias was not
|
||||
referenced yet, fails with `ArgumentError`.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.safe_concat(Unknown, Module)
|
||||
** (ArgumentError) argument error
|
||||
|
||||
iex> Module.safe_concat(List, Chars)
|
||||
List.Chars
|
||||
|
||||
"""
|
||||
@spec safe_concat(binary | atom, binary | atom) :: atom | no_return
|
||||
def safe_concat(left, right) do
|
||||
:elixir_aliases.safe_concat([left, right])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an anonymous function from the given module, function
|
||||
and arity. The module and function are not verified to exist.
|
||||
|
||||
iex> fun = Module.function(Kernel, :is_atom, 1)
|
||||
iex> fun.(:hello)
|
||||
true
|
||||
|
||||
"""
|
||||
def function(mod, fun, arity) do
|
||||
:erlang.make_fun(mod, fun, arity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attaches documentation to a given function or type. It expects
|
||||
the module the function/type belongs to, the line (a non negative
|
||||
integer), the kind (`def` or `defmacro`), a tuple representing
|
||||
the function and its arity, the function signature (the signature
|
||||
should be omitted for types) and the documentation, which should
|
||||
be either a binary or a boolean.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.add_doc(__MODULE__, __ENV__.line + 1, :def, {:version, 0}, [], "Manually added docs")
|
||||
def version, do: 1
|
||||
end
|
||||
|
||||
"""
|
||||
def add_doc(module, line, kind, tuple, signature \\ [], doc)
|
||||
|
||||
def add_doc(_module, _line, kind, _tuple, _signature, doc) when kind in [:defp, :defmacrop, :typep] do
|
||||
if doc, do: {:error, :private_doc}, else: :ok
|
||||
end
|
||||
|
||||
def add_doc(module, line, kind, tuple, signature, doc) when
|
||||
kind in [:def, :defmacro, :type, :opaque] and (is_binary(doc) or is_boolean(doc) or doc == nil) do
|
||||
assert_not_compiled!(:add_doc, module)
|
||||
table = docs_table_for(module)
|
||||
|
||||
{signature, _} = Enum.map_reduce signature, 1, fn(x, acc) ->
|
||||
{simplify_signature(x, acc), acc + 1}
|
||||
end
|
||||
|
||||
case :ets.lookup(table, tuple) do
|
||||
[] ->
|
||||
:ets.insert(table, {tuple, line, kind, signature, doc})
|
||||
:ok
|
||||
[{tuple, line, _old_kind, old_sign, old_doc}] ->
|
||||
:ets.insert(table, {
|
||||
tuple,
|
||||
line,
|
||||
kind,
|
||||
merge_signatures(old_sign, signature, 1),
|
||||
if(nil?(doc), do: old_doc, else: doc)
|
||||
})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Simplify signatures to be stored in docs
|
||||
|
||||
defp simplify_signature({:\\, _, [left, right ]}, i) do
|
||||
{:\\, [], [simplify_signature(left, i), right]}
|
||||
end
|
||||
|
||||
defp simplify_signature({:%, _, [left, _]}, _i) when is_atom(left) do
|
||||
last = List.last(String.split(atom_to_binary(left), "."))
|
||||
atom = binary_to_atom(downcase(last))
|
||||
{atom, [], nil}
|
||||
end
|
||||
|
||||
defp simplify_signature({:=, _, [_, right]}, i) do
|
||||
simplify_signature(right, i)
|
||||
end
|
||||
|
||||
defp simplify_signature({var, _, atom}, _i) when is_atom(atom) do
|
||||
case atom_to_binary(var) do
|
||||
"_" <> rest -> {binary_to_atom(rest), [], Elixir}
|
||||
_ -> {var, [], nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp simplify_signature(other, i) when is_integer(other), do: {:"int#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_boolean(other), do: {:"bool#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_atom(other), do: {:"atom#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_list(other), do: {:"list#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_float(other), do: {:"float#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_binary(other), do: {:"binary#{i}", [], Elixir}
|
||||
defp simplify_signature(_, i), do: {:"arg#{i}", [], Elixir}
|
||||
|
||||
defp downcase(<<c :: utf8, rest :: binary>>) when c >= ?A and c <= ?Z do
|
||||
<<c + 32 :: utf8, downcase(rest) :: binary>>
|
||||
end
|
||||
|
||||
defp downcase(<<c, rest :: binary>>) do
|
||||
<<c, downcase(rest) :: binary>>
|
||||
end
|
||||
|
||||
defp downcase(<<>>) do
|
||||
<<>>
|
||||
end
|
||||
|
||||
# Merge
|
||||
|
||||
defp merge_signatures([h1|t1], [h2|t2], i) do
|
||||
[merge_signature(h1, h2, i)|merge_signatures(t1, t2, i + 1)]
|
||||
end
|
||||
|
||||
defp merge_signatures([], [], _) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp merge_signature({:\\, line, [left, right]}, newer, i) do
|
||||
{:\\, line, [merge_signature(left, newer, i), right]}
|
||||
end
|
||||
|
||||
defp merge_signature(older, {:\\, _, [left, _]}, i) do
|
||||
merge_signature(older, left, i)
|
||||
end
|
||||
|
||||
# The older signature, when given, always have higher precedence
|
||||
defp merge_signature({_, _, nil} = older, _newer, _), do: older
|
||||
defp merge_signature(_older, {_, _, nil} = newer, _), do: newer
|
||||
|
||||
# Both are a guess, so check if they are the same guess
|
||||
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
|
||||
|
||||
# Otherwise, returns a generic guess
|
||||
defp merge_signature({_, line, _}, _newer, i), do: {:"arg#{i}", line, Elixir}
|
||||
|
||||
@doc """
|
||||
Checks if the module defines the given function or macro.
|
||||
Use `defines?/3` to assert for a specific type.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
Module.defines? __MODULE__, {:version, 0} #=> false
|
||||
def version, do: 1
|
||||
Module.defines? __MODULE__, {:version, 0} #=> true
|
||||
end
|
||||
|
||||
"""
|
||||
def defines?(module, tuple) when is_tuple(tuple) do
|
||||
assert_not_compiled!(:defines?, module)
|
||||
table = function_table_for(module)
|
||||
:ets.lookup(table, tuple) != []
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the module defines a function or macro of the
|
||||
given `kind`. `kind` can be any of `:def`, `:defp`,
|
||||
`:defmacro` or `:defmacrop`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
Module.defines? __MODULE__, {:version, 0}, :defp #=> false
|
||||
def version, do: 1
|
||||
Module.defines? __MODULE__, {:version, 0}, :defp #=> false
|
||||
end
|
||||
|
||||
"""
|
||||
def defines?(module, tuple, kind) do
|
||||
assert_not_compiled!(:defines?, module)
|
||||
table = function_table_for(module)
|
||||
case :ets.lookup(table, tuple) do
|
||||
[{_, ^kind, _, _, _, _, _}] -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return all functions defined in `module`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
def version, do: 1
|
||||
Module.definitions_in __MODULE__ #=> [{:version,0}]
|
||||
end
|
||||
|
||||
"""
|
||||
def definitions_in(module) do
|
||||
assert_not_compiled!(:definitions_in, module)
|
||||
table = function_table_for(module)
|
||||
for {tuple, _, _, _, _, _, _} <- :ets.tab2list(table), do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all functions defined in `module`, according
|
||||
to its kind.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
def version, do: 1
|
||||
Module.definitions_in __MODULE__, :def #=> [{:version,0}]
|
||||
Module.definitions_in __MODULE__, :defp #=> []
|
||||
end
|
||||
|
||||
"""
|
||||
def definitions_in(module, kind) do
|
||||
assert_not_compiled!(:definitions_in, module)
|
||||
table = function_table_for(module)
|
||||
for {tuple, stored_kind, _, _, _, _, _} <- :ets.tab2list(table), stored_kind == kind, do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Makes the given functions in `module` overridable.
|
||||
An overridable function is lazily defined, allowing a
|
||||
developer to customize it. See `Kernel.defoverridable/1` for
|
||||
more information and documentation.
|
||||
"""
|
||||
def make_overridable(module, tuples) do
|
||||
assert_not_compiled!(:make_overridable, module)
|
||||
|
||||
for tuple <- tuples do
|
||||
case :elixir_def.lookup_definition(module, tuple) do
|
||||
false ->
|
||||
{name, arity} = tuple
|
||||
raise "Cannot make function #{name}/#{arity} overridable because it was not defined"
|
||||
clause ->
|
||||
:elixir_def.delete_definition(module, tuple)
|
||||
|
||||
neighbours = if loaded?(Module.LocalsTracker) do
|
||||
Module.LocalsTracker.yank(module, tuple)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
old = get_attribute(module, :__overridable)
|
||||
merged = :orddict.update(tuple, fn({count, _, _, _}) ->
|
||||
{count + 1, clause, neighbours, false}
|
||||
end, {1, clause, neighbours, false}, old)
|
||||
|
||||
put_attribute(module, :__overridable, merged)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if `tuple` in `module` is marked as overridable.
|
||||
"""
|
||||
def overridable?(module, tuple) do
|
||||
!!List.keyfind(get_attribute(module, :__overridable), tuple, 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts an Erlang attribute to the given module with the given
|
||||
key and value. The semantics of putting the attribute depends
|
||||
if the attribute was registered or not via `register_attribute/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.put_attribute __MODULE__, :custom_threshold_for_lib, 10
|
||||
end
|
||||
|
||||
"""
|
||||
def put_attribute(module, key, value) when is_atom(key) do
|
||||
assert_not_compiled!(:put_attribute, module)
|
||||
table = data_table_for(module)
|
||||
value = normalize_attribute(key, value)
|
||||
acc = :ets.lookup_element(table, :__acc_attributes, 2)
|
||||
|
||||
new =
|
||||
if :lists.member(key, acc) do
|
||||
case :ets.lookup(table, key) do
|
||||
[{^key, old}] -> [value|old]
|
||||
[] -> [value]
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
:ets.insert(table, {key, new})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the given attribute from a module. If the attribute
|
||||
was marked with `accumulate` with `Module.register_attribute/3`,
|
||||
a list is always returned.
|
||||
|
||||
The `@` macro compiles to a call to this function. For example,
|
||||
the following code:
|
||||
|
||||
@foo
|
||||
|
||||
Expands to:
|
||||
|
||||
Module.get_attribute(__MODULE__, :foo, true)
|
||||
|
||||
Notice the third argument may be given to indicate a stacktrace
|
||||
to be emitted when the attribute was not previously defined.
|
||||
The default value for `warn` is nil for direct calls but the `@foo`
|
||||
macro sets it to the proper stacktrace automatically, warning
|
||||
every time `@foo` is used but not set previously.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
Module.put_attribute __MODULE__, :value, 1
|
||||
Module.get_attribute __MODULE__, :value #=> 1
|
||||
|
||||
Module.register_attribute __MODULE__, :value, accumulate: true
|
||||
Module.put_attribute __MODULE__, :value, 1
|
||||
Module.get_attribute __MODULE__, :value #=> [1]
|
||||
end
|
||||
|
||||
"""
|
||||
@spec get_attribute(module, atom, warn :: nil | [tuple]) :: term
|
||||
def get_attribute(module, key, warn \\ nil) when
|
||||
is_atom(key) and (is_list(warn) or nil?(warn)) do
|
||||
assert_not_compiled!(:get_attribute, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
case :ets.lookup(table, key) do
|
||||
[{^key, val}] -> val
|
||||
[] ->
|
||||
acc = :ets.lookup_element(table, :__acc_attributes, 2)
|
||||
|
||||
cond do
|
||||
:lists.member(key, acc) ->
|
||||
[]
|
||||
is_list(warn) ->
|
||||
:elixir_errors.warn warn_info(warn), "undefined module attribute @#{key}, " <>
|
||||
"please remove access to @#{key} or explicitly set it to nil before access\n"
|
||||
nil
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp warn_info([entry|_]) do
|
||||
opts = elem(entry, size(entry) - 1)
|
||||
Exception.format_file_line(Keyword.get(opts, :file), Keyword.get(opts, :line)) <> " "
|
||||
end
|
||||
|
||||
defp warn_info([]) do
|
||||
""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all attributes that match the given key.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.put_attribute __MODULE__, :custom_threshold_for_lib, 10
|
||||
Module.delete_attribute __MODULE__, :custom_threshold_for_lib
|
||||
end
|
||||
|
||||
"""
|
||||
def delete_attribute(module, key) when is_atom(key) do
|
||||
assert_not_compiled!(:delete_attribute, module)
|
||||
table = data_table_for(module)
|
||||
:ets.delete(table, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers an attribute. By registering an attribute, a developer
|
||||
is able to customize how Elixir will store and accumulate the
|
||||
attribute values.
|
||||
|
||||
## Options
|
||||
|
||||
When registering an attribute, two options can be given:
|
||||
|
||||
* `:accumulate` - Several calls to the same attribute will
|
||||
accumulate instead of override the previous one. New attributes
|
||||
are always added to the top of the accumulated list.
|
||||
|
||||
* `:persist` - The attribute will be persisted in the Erlang
|
||||
Abstract Format. Useful when interfacing with Erlang libraries.
|
||||
|
||||
By default, both options are `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.register_attribute __MODULE__,
|
||||
:custom_threshold_for_lib,
|
||||
accumulate: true, persist: false
|
||||
|
||||
@custom_threshold_for_lib 10
|
||||
@custom_threshold_for_lib 20
|
||||
@custom_threshold_for_lib #=> [20, 10]
|
||||
end
|
||||
|
||||
"""
|
||||
def register_attribute(module, new, opts) when is_atom(new) do
|
||||
assert_not_compiled!(:register_attribute, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
if Keyword.get(opts, :persist) do
|
||||
old = :ets.lookup_element(table, :__persisted_attributes, 2)
|
||||
:ets.insert(table, {:__persisted_attributes, [new|old]})
|
||||
end
|
||||
|
||||
if Keyword.get(opts, :accumulate) do
|
||||
old = :ets.lookup_element(table, :__acc_attributes, 2)
|
||||
:ets.insert(table, {:__acc_attributes, [new|old]})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Split the given module name into binary parts.
|
||||
|
||||
## Examples
|
||||
|
||||
Module.split Very.Long.Module.Name.And.Even.Longer
|
||||
#=> ["Very", "Long", "Module", "Name", "And", "Even", "Longer"]
|
||||
|
||||
"""
|
||||
def split(module) do
|
||||
tl(String.split(String.Chars.to_string(module), "."))
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used internally to compile documentation. This function
|
||||
# is private and must be used only internally.
|
||||
def compile_doc(env, kind, name, args, _guards, _body) do
|
||||
module = env.module
|
||||
line = env.line
|
||||
arity = length(args)
|
||||
pair = {name, arity}
|
||||
doc = get_attribute(module, :doc)
|
||||
|
||||
case add_doc(module, line, kind, pair, args, doc) do
|
||||
:ok ->
|
||||
:ok
|
||||
{:error, :private_doc} ->
|
||||
:elixir_errors.warn line, env.file, "function #{name}/#{arity} is private, @doc's are always discarded for private functions\n"
|
||||
end
|
||||
|
||||
delete_attribute(module, :doc)
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used internally to compile types. This function
|
||||
# is private and must be used only internally.
|
||||
def compile_typespec(module, key, value) when is_atom(key) do
|
||||
assert_not_compiled!(:put_attribute, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
new =
|
||||
case :ets.lookup(table, key) do
|
||||
[{^key, old}] -> [value|old]
|
||||
[] -> [value]
|
||||
end
|
||||
|
||||
:ets.insert(table, {key, new})
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp normalize_attribute(:on_load, atom) when is_atom(atom) do
|
||||
{atom, 0}
|
||||
end
|
||||
|
||||
defp normalize_attribute(:behaviour, atom) when is_atom(atom) do
|
||||
Code.ensure_compiled(atom)
|
||||
atom
|
||||
end
|
||||
|
||||
defp normalize_attribute(:file, file) when is_binary(file) do
|
||||
file
|
||||
end
|
||||
|
||||
defp normalize_attribute(key, atom) when is_atom(atom) and
|
||||
key in [:before_compile, :after_compile, :on_definition] do
|
||||
{atom, :"__#{key}__"}
|
||||
end
|
||||
|
||||
defp normalize_attribute(key, _value) when key in [:type, :typep, :export_type, :opaque, :callback] do
|
||||
raise ArgumentError, message: "attributes type, typep, export_type, opaque and callback " <>
|
||||
"must be set via Kernel.Typespec"
|
||||
end
|
||||
|
||||
defp normalize_attribute(_key, value) do
|
||||
value
|
||||
end
|
||||
|
||||
defp data_table_for(module) do
|
||||
module
|
||||
end
|
||||
|
||||
defp function_table_for(module) do
|
||||
:elixir_def.table(module)
|
||||
end
|
||||
|
||||
defp docs_table_for(module) do
|
||||
:elixir_module.docs_table(module)
|
||||
end
|
||||
|
||||
defp assert_not_compiled!(fun, module) do
|
||||
open?(module) ||
|
||||
raise ArgumentError,
|
||||
message: "could not call #{fun} on module #{inspect module} because it was already compiled"
|
||||
end
|
||||
|
||||
defp loaded?(module), do: is_tuple :code.is_loaded(module)
|
||||
end
|
||||
@@ -1,330 +0,0 @@
|
||||
# This is a module Elixir responsible for tracking
|
||||
# calls in order to extract Elixir modules' behaviour
|
||||
# during compilation time.
|
||||
#
|
||||
# ## Implementation
|
||||
#
|
||||
# The implementation uses the digraph module to track
|
||||
# all dependencies. The graph starts with one main vertice:
|
||||
#
|
||||
# * `:local` - points to local functions
|
||||
#
|
||||
# We also have can the following vertices:
|
||||
#
|
||||
# * `Module` - a module that was invoked via an import
|
||||
# * `{name, arity}` - a local function/arity pair
|
||||
# * `{:import, name, arity}` - an invoked function/arity import
|
||||
#
|
||||
# Each of those vertices can associate to other vertices
|
||||
# as described below:
|
||||
#
|
||||
# * `Module`
|
||||
# * in neighbours: `{:import, name, arity}`
|
||||
#
|
||||
# * `{name, arity}`
|
||||
# * in neighbours: `:local`, `{name, arity}`
|
||||
# * out neighbours: `{:import, name, arity}`
|
||||
#
|
||||
# * `{:import, name, arity}`
|
||||
# * in neighbours: `{name, arity}`
|
||||
# * out neighbours: `Module`
|
||||
#
|
||||
# Note that since this is required for bootstrap, we can't use
|
||||
# any of the `GenServer.Behaviour` conveniences.
|
||||
defmodule Module.LocalsTracker do
|
||||
@moduledoc false
|
||||
|
||||
@timeout 30_000
|
||||
@behaviour :gen_server
|
||||
|
||||
@type ref :: pid | module
|
||||
@type name :: atom
|
||||
@type name_arity :: {name, arity}
|
||||
|
||||
@type local :: {name, arity}
|
||||
@type import :: {:import, name, arity}
|
||||
|
||||
# Public API
|
||||
|
||||
@doc """
|
||||
Returns all imported modules that had the given
|
||||
`{name, arity}` invoked.
|
||||
"""
|
||||
@spec imports_with_dispatch(ref, name_arity) :: [module]
|
||||
def imports_with_dispatch(ref, {name, arity}) do
|
||||
d = :gen_server.call(to_pid(ref), :digraph, @timeout)
|
||||
:digraph.out_neighbours(d, {:import, name, arity})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all locals that are reachable.
|
||||
|
||||
By default, all public functions are reachable.
|
||||
A private function is only reachable if it has
|
||||
a public function that it invokes directly.
|
||||
"""
|
||||
@spec reachable(ref) :: [local]
|
||||
def reachable(ref) do
|
||||
d = :gen_server.call(to_pid(ref), :digraph, @timeout)
|
||||
reduce_reachable(d, :local, [])
|
||||
end
|
||||
|
||||
defp reduce_reachable(d, vertex, vertices) do
|
||||
neighbours = :digraph.out_neighbours(d, vertex)
|
||||
neighbours = (for {_, _} = t <- neighbours, do: t) |> :ordsets.from_list
|
||||
remaining = :ordsets.subtract(neighbours, vertices)
|
||||
vertices = :ordsets.union(neighbours, vertices)
|
||||
:lists.foldl(&reduce_reachable(d, &1, &2), vertices, remaining)
|
||||
end
|
||||
|
||||
defp to_pid(pid) when is_pid(pid), do: pid
|
||||
defp to_pid(mod) when is_atom(mod) do
|
||||
table = :elixir_module.data_table(mod)
|
||||
[{_, val}] = :ets.lookup(table, :__locals_tracker)
|
||||
val
|
||||
end
|
||||
|
||||
# Internal API
|
||||
|
||||
# Starts the tracker and returns its pid.
|
||||
@doc false
|
||||
def start_link do
|
||||
{:ok, pid} = :gen_server.start_link(__MODULE__, [], [])
|
||||
pid
|
||||
end
|
||||
|
||||
# Adds a definition into the tracker. A public
|
||||
# definition is connected with the :local node
|
||||
# while a private one is left unreachable until
|
||||
# a call is made to.
|
||||
@doc false
|
||||
def add_definition(pid, kind, tuple) when kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
:gen_server.cast(pid, {:add_definition, kind, tuple})
|
||||
end
|
||||
|
||||
# Adds and tracks defaults for a definition into the tracker.
|
||||
@doc false
|
||||
def add_defaults(pid, kind, tuple, defaults) when kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
:gen_server.cast(pid, {:add_defaults, kind, tuple, defaults})
|
||||
end
|
||||
|
||||
# Adds a local dispatch to the given target.
|
||||
def add_local(pid, to) when is_tuple(to) do
|
||||
:gen_server.cast(pid, {:add_local, :local, to})
|
||||
end
|
||||
|
||||
# Adds a local dispatch from-to the given target.
|
||||
@doc false
|
||||
def add_local(pid, from, to) when is_tuple(from) and is_tuple(to) do
|
||||
:gen_server.cast(pid, {:add_local, from, to})
|
||||
end
|
||||
|
||||
# Adds a import dispatch to the given target.
|
||||
@doc false
|
||||
def add_import(pid, function, module, target) when is_atom(module) and is_tuple(target) do
|
||||
:gen_server.cast(pid, {:add_import, function, module, target})
|
||||
end
|
||||
|
||||
# Yanks a local node. Returns its in and out vertices in a tuple.
|
||||
@doc false
|
||||
def yank(pid, local) do
|
||||
:gen_server.call(to_pid(pid), {:yank, local}, @timeout)
|
||||
end
|
||||
|
||||
# Reattach a previously yanked node
|
||||
@doc false
|
||||
def reattach(pid, kind, tuple, neighbours) do
|
||||
pid = to_pid(pid)
|
||||
add_definition(pid, kind, tuple)
|
||||
:gen_server.cast(pid, {:reattach, tuple, neighbours})
|
||||
end
|
||||
|
||||
# Collecting all conflicting imports with the given functions
|
||||
@doc false
|
||||
def collect_imports_conflicts(pid, all_defined) do
|
||||
d = :gen_server.call(pid, :digraph, @timeout)
|
||||
|
||||
for {name, arity} <- all_defined,
|
||||
:digraph.in_neighbours(d, {:import, name, arity}) != [],
|
||||
n = :digraph.out_neighbours(d, {:import, name, arity}),
|
||||
n != [] do
|
||||
{n, name, arity}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect all unused definitions based on the private
|
||||
# given also accounting the expected amount of default
|
||||
# clauses a private function have.
|
||||
@doc false
|
||||
def collect_unused_locals(pid, private) do
|
||||
reachable = reachable(pid)
|
||||
:lists.foldl(&collect_unused_locals(&1, &2, reachable), [], private)
|
||||
end
|
||||
|
||||
defp collect_unused_locals({tuple, kind, 0}, acc, reachable) do
|
||||
if :lists.member(tuple, reachable) do
|
||||
acc
|
||||
else
|
||||
[{:unused_def, tuple, kind}|acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_unused_locals({tuple, kind, default}, acc, reachable) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
invoked = for {n, a} <- reachable, n == name, a in min..max, do: a
|
||||
|
||||
if invoked == [] do
|
||||
[{:unused_def, tuple, kind}|acc]
|
||||
else
|
||||
case :lists.min(invoked) - min do
|
||||
0 -> acc
|
||||
^default -> [{:unused_args, tuple}|acc]
|
||||
unused_args -> [{:unused_args, tuple, unused_args}|acc]
|
||||
end
|
||||
end
|
||||
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
|
||||
:gen_server.cast(pid, :stop)
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init([]) do
|
||||
d = :digraph.new([:protected])
|
||||
:digraph.add_vertex(d, :local)
|
||||
{:ok, {d, []}}
|
||||
end
|
||||
|
||||
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
|
||||
in_vertices = :digraph.in_neighbours(d, local)
|
||||
out_vertices = :digraph.out_neighbours(d, local)
|
||||
:digraph.del_vertex(d, local)
|
||||
{:reply, {in_vertices, out_vertices}, state}
|
||||
end
|
||||
|
||||
def handle_call(:digraph, _from, {d, _} = state) do
|
||||
{:reply, d, state}
|
||||
end
|
||||
|
||||
def handle_call(request, _from, state) do
|
||||
{:stop, {:bad_call, request}, state}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_local, from, to}, {d, _} = state) do
|
||||
handle_add_local(d, from, to)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, function, module, {name, arity}}, {d, _} = state) do
|
||||
handle_import(d, function, module, name, arity)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_definition, kind, tuple}, {d, _} = state) do
|
||||
handle_add_definition(d, kind, tuple)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_defaults, kind, {name, arity}, defaults}, {d, _} = state) do
|
||||
for i <- :lists.seq(arity - defaults, arity - 1) do
|
||||
handle_add_definition(d, kind, {name, i})
|
||||
handle_add_local(d, {name, i}, {name, i + 1})
|
||||
end
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:reattach, tuple, {in_neigh, out_neigh}}, {d, _} = state) do
|
||||
for from <- in_neigh, do: replace_edge(d, from, tuple)
|
||||
for to <- out_neigh, do: replace_edge(d, tuple, to)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast(:stop, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast(msg, state) do
|
||||
{:stop, {:bad_cast, msg}, state}
|
||||
end
|
||||
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defp handle_import(d, function, module, name, arity) do
|
||||
:digraph.add_vertex(d, module)
|
||||
|
||||
tuple = {:import, name, arity}
|
||||
:digraph.add_vertex(d, tuple)
|
||||
replace_edge!(d, tuple, module)
|
||||
|
||||
if function != nil do
|
||||
replace_edge!(d, function, tuple)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_add_local(d, from, to) do
|
||||
:digraph.add_vertex(d, to)
|
||||
replace_edge!(d, from, to)
|
||||
end
|
||||
|
||||
defp handle_add_definition(d, public, tuple) when public in [:def, :defmacro] do
|
||||
:digraph.add_vertex(d, tuple)
|
||||
replace_edge!(d, :local, tuple)
|
||||
end
|
||||
|
||||
defp handle_add_definition(d, private, tuple) when private in [:defp, :defmacrop] do
|
||||
:digraph.add_vertex(d, tuple)
|
||||
end
|
||||
|
||||
defp replace_edge!(d, from, to) do
|
||||
unless :lists.member(to, :digraph.out_neighbours(d, from)) do
|
||||
[:"$e"|_] = :digraph.add_edge(d, from, to)
|
||||
end
|
||||
end
|
||||
|
||||
defp replace_edge(d, from, to) do
|
||||
unless :lists.member(to, :digraph.out_neighbours(d, from)) do
|
||||
:digraph.add_edge(d, from, to)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,234 +0,0 @@
|
||||
defmodule Node do
|
||||
@moduledoc """
|
||||
Functions related to VM nodes.
|
||||
|
||||
Some of the functions in this module are inlined by the compiler,
|
||||
similar to functions in the `Kernel` module and they are explicitly
|
||||
marked in their docs as "inlined by the compiler". For more information
|
||||
about inlined functions, check out the `Kernel` module.
|
||||
"""
|
||||
|
||||
@type t :: node
|
||||
|
||||
@doc """
|
||||
Returns the current node.
|
||||
|
||||
It returns the same as the built-in `node()`.
|
||||
"""
|
||||
@spec self :: t
|
||||
def self do
|
||||
:erlang.node()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if the local node is alive.
|
||||
|
||||
That is, if the node can be part of a distributed system.
|
||||
"""
|
||||
@spec alive? :: boolean
|
||||
def alive? do
|
||||
:erlang.is_alive()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all visible nodes in the system, excluding
|
||||
the local node.
|
||||
|
||||
Same as `list(:visible)`.
|
||||
"""
|
||||
@spec list :: [t]
|
||||
def list do
|
||||
:erlang.nodes()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of nodes according to argument given.
|
||||
|
||||
The result returned when the argument is a list, is the list of nodes
|
||||
satisfying the disjunction(s) of the list elements.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#nodes-1 for more info.
|
||||
"""
|
||||
@typep list_arg :: :visible | :hidden | :connected | :this | :known
|
||||
@spec list(list_arg | [list_arg]) :: [t]
|
||||
def list(args) do
|
||||
:erlang.nodes(args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Monitors the status of the node.
|
||||
|
||||
If `flag` is `true`, monitoring is turned on.
|
||||
If `flag` is `false`, monitoring is turned off.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor_node-2 for more info.
|
||||
"""
|
||||
@spec monitor(t, boolean) :: true
|
||||
def monitor(node, flag) do
|
||||
:erlang.monitor_node(node, flag)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Behaves as `monitor/2` except that it allows an extra
|
||||
option to be given, namely `:allow_passive_connect`.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor_node-3 for more info.
|
||||
"""
|
||||
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
|
||||
def monitor(node, flag, options) do
|
||||
:erlang.monitor_node(node, flag, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Tries to set up a connection to node.
|
||||
|
||||
Returns `:pang` if it fails, or `:pong` if it is successful.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Node.ping(:unknown_node)
|
||||
:pang
|
||||
|
||||
"""
|
||||
@spec ping(t) :: :pong | :pang
|
||||
def ping(node) do
|
||||
:net_adm.ping(node)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Forces the disconnection of a node.
|
||||
|
||||
This will appear to the `node` as if the local node has crashed.
|
||||
This function is mainly used in the Erlang network authentication
|
||||
protocols. Returns `true` if disconnection succeeds, otherwise `false`.
|
||||
If the local node is not alive, the function returns `:ignored`.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#disconnect_node-1 for more info.
|
||||
"""
|
||||
@spec disconnect(t) :: boolean | :ignored
|
||||
def disconnect(node) do
|
||||
:erlang.disconnect_node(node)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Establishes a connection to `node`.
|
||||
|
||||
Returns `true` if successful, `false` if not, and the atom
|
||||
`:ignored` if the local node is not alive.
|
||||
|
||||
See http://erlang.org/doc/man/net_kernel.html#connect_node-1 for more info.
|
||||
"""
|
||||
@spec connect(t) :: boolean | :ignored
|
||||
def connect(node) do
|
||||
:net_kernel.connect_node(node)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
on `node`. If `node` does not exist, a useless pid is returned.
|
||||
|
||||
Check http://www.erlang.org/doc/man/erlang.html#spawn-2 for
|
||||
the list of available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, (() -> any)) :: pid
|
||||
def spawn(node, fun) do
|
||||
:erlang.spawn(node, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
on `node`.
|
||||
|
||||
If `node` does not exist, a useless pid is returned. Check
|
||||
http://www.erlang.org/doc/man/erlang.html#spawn_opt-3 for the list of
|
||||
available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, (() -> any), Process.spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(node, fun, opts) do
|
||||
:erlang.spawn_opt(node, fun, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)` on `node`.
|
||||
|
||||
If `node` does not exist, a useless pid is returned. Check
|
||||
http://www.erlang.org/doc/man/erlang.html#spawn-4 for the list of
|
||||
available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, module, atom, [any]) :: pid
|
||||
def spawn(node, module, fun, args) do
|
||||
:erlang.spawn(node, module, fun, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)` on `node`.
|
||||
|
||||
If `node` does not exist, a useless pid is returned. Check
|
||||
http://www.erlang.org/doc/man/erlang.html#spawn_opt-5 for the list of
|
||||
available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, module, atom, [any], Process.spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(node, module, fun, args, opts) do
|
||||
:erlang.spawn_opt(node, module, fun, args, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new linked process started by the application of `fun` on `node`.
|
||||
|
||||
A link is created between the calling process and the new process, atomically.
|
||||
If `node` does not exist, a useless pid is returned (and due to the link, an exit
|
||||
signal with exit reason `:noconnection` will be received).
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn_link(t, (() -> any)) :: pid
|
||||
def spawn_link(node, fun) do
|
||||
:erlang.spawn_link(node, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new linked process started by the application of
|
||||
`module.function(args)` on `node`.
|
||||
|
||||
A link is created between the calling process and the new process, atomically.
|
||||
If `node` does not exist, a useless pid is returned (and due to the link, an exit
|
||||
signal with exit reason `:noconnection` will be received).
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn_link(t, module, atom, [any]) :: pid
|
||||
def spawn_link(node, module, fun, args) do
|
||||
:erlang.spawn_link(node, module, fun, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the magic cookie of `node` to the atom `cookie`.
|
||||
|
||||
The default node is `Node.self`, the local node. If `node` is the local node,
|
||||
the function also sets the cookie of all other unknown nodes to `cookie`.
|
||||
|
||||
This function will raise `FunctionClauseError` if the given `node` is not alive.
|
||||
"""
|
||||
def set_cookie(node \\ Node.self, cookie) when is_atom(cookie) do
|
||||
:erlang.set_cookie(node, cookie)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the magic cookie of the local node.
|
||||
|
||||
Returns the cookie if the node is alive, otherwise `:nocookie`.
|
||||
"""
|
||||
def get_cookie() do
|
||||
:erlang.get_cookie()
|
||||
end
|
||||
end
|
||||
@@ -1,260 +0,0 @@
|
||||
defmodule OptionParser do
|
||||
@moduledoc """
|
||||
This module contains functions to parse command line arguments.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Parses `argv` into a keywords list.
|
||||
|
||||
It returns the parsed values, remaining arguments and the
|
||||
invalid options.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> OptionParser.parse(["--debug"])
|
||||
{[debug: true], [], []}
|
||||
|
||||
iex> OptionParser.parse(["--source", "lib"])
|
||||
{[source: "lib"], [], []}
|
||||
|
||||
iex> OptionParser.parse(["--source-path", "lib", "test/enum_test.exs", "--verbose"])
|
||||
{[source_path: "lib", verbose: true], ["test/enum_test.exs"], []}
|
||||
|
||||
By default, Elixir will try to automatically parse switches.
|
||||
Switches without an argument, like `--debug` will automatically
|
||||
be set to true. Switches followed by a value will be assigned
|
||||
to the value, always as strings.
|
||||
|
||||
Note Elixir also converts the switches to underscore atoms, as
|
||||
`--source-path` becomes `:source_path`, to better suit Elixir
|
||||
conventions.
|
||||
|
||||
## Switches
|
||||
|
||||
Many times though, it is better to explicitly list the available
|
||||
switches and their formats. The following types are supported:
|
||||
|
||||
* `:boolean` - Marks the given switch as a boolean. Boolean switches
|
||||
never consume the following value unless it is
|
||||
`true` or `false`;
|
||||
* `:integer` - Parses the switch as an integer;
|
||||
* `:float` - Parses the switch as a float;
|
||||
* `:string` - Returns the switch as a string;
|
||||
|
||||
If a switch can't be parsed, the option is returned in the invalid
|
||||
options list (third element of the returned tuple).
|
||||
|
||||
The following extra "types" are supported:
|
||||
|
||||
* `:keep` - Keeps duplicated items in the list instead of overriding;
|
||||
|
||||
Examples:
|
||||
|
||||
iex> OptionParser.parse(["--unlock", "path/to/file"], switches: [unlock: :boolean])
|
||||
{[unlock: true], ["path/to/file"], []}
|
||||
|
||||
iex> OptionParser.parse(["--unlock", "--limit", "0", "path/to/file"],
|
||||
...> switches: [unlock: :boolean, limit: :integer])
|
||||
{[unlock: true, limit: 0], ["path/to/file"], []}
|
||||
|
||||
iex> OptionParser.parse(["--limit", "3"], switches: [limit: :integer])
|
||||
{[limit: 3], [], []}
|
||||
|
||||
iex> OptionParser.parse(["--limit", "yyz"], switches: [limit: :integer])
|
||||
{[], [], [limit: "yyz"]}
|
||||
|
||||
## Negation switches
|
||||
|
||||
All switches starting with `--no-` are considered to be booleans and never
|
||||
parse the next value:
|
||||
|
||||
iex> OptionParser.parse(["--no-op", "path/to/file"])
|
||||
{[no_op: true], ["path/to/file"], []}
|
||||
|
||||
However, in case the base switch exists, it sets that particular switch to
|
||||
false:
|
||||
|
||||
iex> OptionParser.parse(["--no-op", "path/to/file"], switches: [op: :boolean])
|
||||
{[op: false], ["path/to/file"], []}
|
||||
|
||||
## Aliases
|
||||
|
||||
A set of aliases can be given as options too:
|
||||
|
||||
iex> OptionParser.parse(["-d"], aliases: [d: :debug])
|
||||
{[debug: true], [], []}
|
||||
|
||||
"""
|
||||
def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
||||
parse(argv, opts, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Similar to `parse/2` but only parses the head of `argv`;
|
||||
as soon as it finds a non-switch, it stops parsing.
|
||||
|
||||
See `parse/2` for more information.
|
||||
|
||||
## Example
|
||||
|
||||
iex> OptionParser.parse_head(["--source", "lib", "test/enum_test.exs", "--verbose"])
|
||||
{[source: "lib"], ["test/enum_test.exs", "--verbose"], []}
|
||||
|
||||
iex> OptionParser.parse_head(["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"])
|
||||
{[verbose: true, source: "lib"], ["test/enum_test.exs", "--unlock"], []}
|
||||
|
||||
"""
|
||||
def parse_head(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
||||
parse(argv, opts, false)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp parse(argv, opts, bool) do
|
||||
aliases = opts[:aliases] || []
|
||||
switches = opts[:switches] || []
|
||||
parse(argv, aliases, switches, bool)
|
||||
end
|
||||
|
||||
defp parse(argv, aliases, switches, all) do
|
||||
parse(argv, aliases, switches, [], [], [], all)
|
||||
end
|
||||
|
||||
defp parse(["--"|_] = value, _aliases, _switches, dict, args, invalid, _all) do
|
||||
{Enum.reverse(dict), Enum.reverse(args, value), Enum.reverse(invalid)}
|
||||
end
|
||||
|
||||
defp parse(["-" <> option|t], aliases, switches, dict, args, invalid, all) do
|
||||
{option, value} = split_option(option)
|
||||
{option, kinds, value} = normalize_option(option, value, switches, aliases)
|
||||
{value, kinds, t} = normalize_value(value, kinds, t)
|
||||
{dict, invalid} = store_option(dict, invalid, option, value, kinds)
|
||||
parse(t, aliases, switches, dict, args, invalid, all)
|
||||
end
|
||||
|
||||
defp parse([h|t], aliases, switches, dict, args, invalid, true) do
|
||||
parse(t, aliases, switches, dict, [h|args], invalid, true)
|
||||
end
|
||||
|
||||
defp parse([], _, _switches, dict, args, invalid, true) do
|
||||
{Enum.reverse(dict), Enum.reverse(args), Enum.reverse(invalid)}
|
||||
end
|
||||
|
||||
defp parse(value, _, _switches, dict, _args, invalid, false) do
|
||||
{Enum.reverse(dict), value, Enum.reverse(invalid)}
|
||||
end
|
||||
|
||||
defp store_option(dict, invalid, option, value, kinds) do
|
||||
{invalid_option, value} =
|
||||
cond do
|
||||
:invalid in kinds ->
|
||||
{option, value}
|
||||
:boolean in kinds ->
|
||||
{nil, value in [true, "true"]}
|
||||
:integer in kinds ->
|
||||
case Integer.parse(value) do
|
||||
{value, ""} -> {nil, value}
|
||||
_ -> {option, value}
|
||||
end
|
||||
:float in kinds ->
|
||||
case Float.parse(value) do
|
||||
{value, ""} -> {nil, value}
|
||||
_ -> {option, value}
|
||||
end
|
||||
true ->
|
||||
{nil, value}
|
||||
end
|
||||
|
||||
if invalid_option do
|
||||
{dict, [{option, value}|invalid]}
|
||||
else
|
||||
{do_store_option(dict, option, value, kinds), invalid}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_store_option(dict, option, value, kinds) do
|
||||
cond do
|
||||
:keep in kinds ->
|
||||
[{option, value}|dict]
|
||||
true ->
|
||||
[{option, value}|Keyword.delete(dict, option)]
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_option(<<?-, option :: binary>>, value, switches, _aliases) do
|
||||
normalize_option(get_negated(option, switches), value, switches)
|
||||
end
|
||||
|
||||
defp normalize_option(option, value, switches, aliases) do
|
||||
option = get_option(option)
|
||||
if alias = aliases[option] do
|
||||
normalize_option({:default, alias}, value, switches)
|
||||
else
|
||||
{option, [:invalid], value}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_option({:negated, option}, _value, switches) do
|
||||
kinds = List.wrap(switches[option])
|
||||
|
||||
cond do
|
||||
:boolean in kinds ->
|
||||
{option, kinds, false}
|
||||
kinds == [] ->
|
||||
{option, kinds, true}
|
||||
true ->
|
||||
{option, [:invalid], false}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_option({:default, option}, value, switches) do
|
||||
{option, List.wrap(switches[option]), value}
|
||||
end
|
||||
|
||||
defp normalize_value(nil, kinds, t) do
|
||||
cond do
|
||||
:boolean in kinds ->
|
||||
{true, kinds, t}
|
||||
value_in_tail?(t) ->
|
||||
[h|t] = t
|
||||
{h, kinds, t}
|
||||
kinds == [] ->
|
||||
{true, kinds, t}
|
||||
true ->
|
||||
{true, [:invalid], t}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_value(value, kinds, t) do
|
||||
{value, kinds, t}
|
||||
end
|
||||
|
||||
defp value_in_tail?(["-" <> _|_]), do: false
|
||||
defp value_in_tail?([]), do: false
|
||||
defp value_in_tail?(_), do: true
|
||||
|
||||
defp split_option(option) do
|
||||
case :binary.split(option, "=") do
|
||||
[h] -> {h, nil}
|
||||
[h, t] -> {h, t}
|
||||
end
|
||||
end
|
||||
|
||||
defp to_underscore(option) do
|
||||
for <<c <- option>>, into: "", do: << if(c == ?-, do: ?_, else: c) >>
|
||||
end
|
||||
|
||||
defp get_option(option) do
|
||||
option |> to_underscore |> binary_to_atom
|
||||
end
|
||||
|
||||
defp get_negated("no-" <> rest = option, switches) do
|
||||
negated = get_option(rest)
|
||||
option = if Keyword.has_key?(switches, negated), do: negated, else: get_option(option)
|
||||
{:negated, option}
|
||||
end
|
||||
|
||||
defp get_negated(rest, _switches) do
|
||||
{:default, get_option(rest)}
|
||||
end
|
||||
end
|
||||
@@ -1,549 +0,0 @@
|
||||
defmodule Path do
|
||||
@moduledoc """
|
||||
This module provides conveniences for manipulating or
|
||||
retrieving file system paths.
|
||||
|
||||
The functions in this module may receive a char data as
|
||||
argument (i.e. a string or a list of characters / string)
|
||||
and will always return a string (encoded in UTF-8).
|
||||
|
||||
The majority of the functions in this module do not
|
||||
interact with the file system, except for a few functions
|
||||
that require it (like `wildcard/1` and `expand/1`).
|
||||
"""
|
||||
|
||||
alias :filename, as: FN
|
||||
@type t :: char_data
|
||||
|
||||
@doc """
|
||||
Converts the given path to an absolute one. Unlike
|
||||
`expand/1`, no attempt is made to resolve `..`, `.` or `~`.
|
||||
|
||||
## Unix examples
|
||||
|
||||
Path.absname("foo")
|
||||
#=> "/usr/local/foo"
|
||||
|
||||
Path.absname("../x")
|
||||
#=> "/usr/local/../x"
|
||||
|
||||
## Windows
|
||||
|
||||
Path.absname("foo").
|
||||
"D:/usr/local/foo"
|
||||
Path.absname("../x").
|
||||
"D:/usr/local/../x"
|
||||
|
||||
"""
|
||||
@spec absname(t) :: binary
|
||||
def absname(path) do
|
||||
absname(path, System.cwd!)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a path from `relative_to` to `path`. If `path` is already
|
||||
an absolute path, `relative_to` is ignored. See also `relative_to/2`.
|
||||
|
||||
Unlike `expand/2`, no attempt is made to
|
||||
resolve `..`, `.` or `~`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.absname("foo", "bar")
|
||||
"bar/foo"
|
||||
|
||||
iex> Path.absname("../x", "bar")
|
||||
"bar/../x"
|
||||
|
||||
"""
|
||||
@spec absname(t, t) :: binary
|
||||
def absname(path, relative_to) do
|
||||
path = String.from_char_data!(path)
|
||||
case type(path) do
|
||||
:relative -> join(relative_to, path)
|
||||
:absolute ->
|
||||
if :binary.last(path) == ?/ do
|
||||
binary_part(path, 0, byte_size(path) - 1)
|
||||
else
|
||||
path
|
||||
end
|
||||
:volumerelative ->
|
||||
relative_to = String.from_char_data!(relative_to)
|
||||
absname_vr(split(path), split(relative_to), relative_to)
|
||||
end
|
||||
end
|
||||
|
||||
## Absolute path on current drive
|
||||
defp absname_vr(["/"|rest], [volume|_], _relative),
|
||||
do: join([volume|rest])
|
||||
|
||||
## Relative to current directory on current drive.
|
||||
defp absname_vr([<<x, ?:>>|rest], [<<x, _ :: binary>>|_], relative),
|
||||
do: absname(join(rest), relative)
|
||||
|
||||
## Relative to current directory on another drive.
|
||||
defp absname_vr([<<x, ?:>>|name], _, _relative) do
|
||||
cwd =
|
||||
case :file.get_cwd([x, ?:]) do
|
||||
{:ok, dir} -> String.from_char_data!(dir)
|
||||
{:error, _} -> <<x, ?:, ?/>>
|
||||
end
|
||||
absname(join(name), cwd)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the path to an absolute one and expands
|
||||
any `.` and `..` characters and a leading `~`.
|
||||
|
||||
## Examples
|
||||
|
||||
Path.expand("/foo/bar/../bar")
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec expand(t) :: binary
|
||||
def expand(path) do
|
||||
normalize absname(expand_home(path), System.cwd!)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands the path relative to the path given as the second argument
|
||||
expanding any `.` and `..` characters. If the path is already an
|
||||
absolute path, `relative_to` is ignored.
|
||||
|
||||
Note, that this function treats `path` with a leading `~` as
|
||||
an absolute one.
|
||||
|
||||
The second argument is first expanded to an absolute path.
|
||||
|
||||
## Examples
|
||||
|
||||
# Assuming that the absolute path to baz is /quux/baz
|
||||
Path.expand("foo/bar/../bar", "baz")
|
||||
#=> "/quux/baz/foo/bar"
|
||||
|
||||
Path.expand("foo/bar/../bar", "/baz")
|
||||
"/baz/foo/bar"
|
||||
Path.expand("/foo/bar/../bar", "/baz")
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec expand(t, t) :: binary
|
||||
def expand(path, relative_to) do
|
||||
normalize absname(absname(expand_home(path), expand_home(relative_to)), System.cwd!)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the path type.
|
||||
|
||||
## Unix examples
|
||||
|
||||
Path.type("/usr/local/bin") #=> :absolute
|
||||
Path.type("usr/local/bin") #=> :relative
|
||||
Path.type("../usr/local/bin") #=> :relative
|
||||
Path.type("~/file") #=> :relative
|
||||
|
||||
## Windows examples
|
||||
|
||||
Path.type("D:/usr/local/bin") #=> :absolute
|
||||
Path.type("usr/local/bin") #=> :relative
|
||||
Path.type("D:bar.ex") #=> :volumerelative
|
||||
Path.type("/bar/foo.ex") #=> :volumerelative
|
||||
|
||||
"""
|
||||
@spec type(t) :: :absolute | :relative | :volumerelative
|
||||
def type(name) when is_list(name) or is_binary(name) do
|
||||
case :os.type() do
|
||||
{:win32, _} -> win32_pathtype(name)
|
||||
_ -> unix_pathtype(name)
|
||||
end |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Forces the path to be a relative path.
|
||||
|
||||
## Unix examples
|
||||
|
||||
Path.relative("/usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("../usr/local/bin") #=> "../usr/local/bin"
|
||||
|
||||
## Windows examples
|
||||
|
||||
Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("D:bar.ex") #=> "bar.ex"
|
||||
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
|
||||
|
||||
"""
|
||||
@spec relative(t) :: binary
|
||||
def relative(name) do
|
||||
case :os.type() do
|
||||
{:win32, _} -> win32_pathtype(name)
|
||||
_ -> unix_pathtype(name)
|
||||
end |> elem(1) |> String.from_char_data!
|
||||
end
|
||||
|
||||
defp unix_pathtype(<<?/, relative :: binary>>), do:
|
||||
{:absolute, relative}
|
||||
defp unix_pathtype([?/|relative]), do:
|
||||
{:absolute, relative}
|
||||
defp unix_pathtype([list|rest]) when is_list(list), do:
|
||||
unix_pathtype(list ++ rest)
|
||||
defp unix_pathtype(relative), do:
|
||||
{:relative, relative}
|
||||
|
||||
@slash [?/, ?\\]
|
||||
|
||||
defp win32_pathtype([list|rest]) when is_list(list), do:
|
||||
win32_pathtype(list++rest)
|
||||
defp win32_pathtype([char, list|rest]) when is_list(list), do:
|
||||
win32_pathtype([char|list++rest])
|
||||
defp win32_pathtype(<<c1, c2, relative :: binary>>) when c1 in @slash and c2 in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype(<<c, relative :: binary>>) when c in @slash, do:
|
||||
{:volumerelative, relative}
|
||||
defp win32_pathtype(<<_letter, ?:, c, relative :: binary>>) when c in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype(<<_letter, ?:, relative :: binary>>), do:
|
||||
{:volumerelative, relative}
|
||||
|
||||
defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype([c | relative]) when c in @slash, do:
|
||||
{:volumerelative, relative}
|
||||
defp win32_pathtype([c1, c2, list|rest]) when is_list(list), do:
|
||||
win32_pathtype([c1, c2|list++rest])
|
||||
defp win32_pathtype([_letter, ?:, c | relative]) when c in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype([_letter, ?: | relative]), do:
|
||||
{:volumerelative, relative}
|
||||
defp win32_pathtype(relative), do:
|
||||
{:relative, relative}
|
||||
|
||||
@doc """
|
||||
Returns the given `path` relative to the given `from` path.
|
||||
In other words, it tries to strip the `from` prefix from `path`.
|
||||
|
||||
This function does not query the file system, so it assumes
|
||||
no symlinks in between the paths.
|
||||
|
||||
In case a direct relative path cannot be found, it returns
|
||||
the original path.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/usr/local")
|
||||
"foo"
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/")
|
||||
"usr/local/foo"
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/etc")
|
||||
"/usr/local/foo"
|
||||
|
||||
"""
|
||||
@spec relative_to(t, t) :: binary
|
||||
def relative_to(path, from) do
|
||||
path = String.from_char_data!(path)
|
||||
relative_to(split(path), split(from), path)
|
||||
end
|
||||
|
||||
defp relative_to([h|t1], [h|t2], original) do
|
||||
relative_to(t1, t2, original)
|
||||
end
|
||||
|
||||
defp relative_to([_|_] = l1, [], _original) do
|
||||
join(l1)
|
||||
end
|
||||
|
||||
defp relative_to(_, _, original) do
|
||||
original
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convenience to get the path relative to the current working
|
||||
directory. If, for some reason, the current working directory
|
||||
cannot be retrieved, returns the full path.
|
||||
"""
|
||||
@spec relative_to_cwd(t) :: binary
|
||||
def relative_to_cwd(path) do
|
||||
case :file.get_cwd do
|
||||
{:ok, base} -> relative_to(path, String.from_char_data!(base))
|
||||
_ -> path
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last component of the path or the path
|
||||
itself if it does not contain any directory separators.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.basename("foo")
|
||||
"foo"
|
||||
|
||||
iex> Path.basename("foo/bar")
|
||||
"bar"
|
||||
|
||||
iex> Path.basename("/")
|
||||
""
|
||||
|
||||
"""
|
||||
@spec basename(t) :: binary
|
||||
def basename(path) do
|
||||
FN.basename(String.from_char_data!(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last component of `path` with the `extension`
|
||||
stripped. This function should be used to remove a specific
|
||||
extension which may, or may not, be there.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.basename("~/foo/bar.ex", ".ex")
|
||||
"bar"
|
||||
|
||||
iex> Path.basename("~/foo/bar.exs", ".ex")
|
||||
"bar.exs"
|
||||
|
||||
iex> Path.basename("~/foo/bar.old.ex", ".ex")
|
||||
"bar.old"
|
||||
|
||||
"""
|
||||
@spec basename(t, t) :: binary
|
||||
def basename(path, extension) do
|
||||
FN.basename(String.from_char_data!(path), String.from_char_data!(extension))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the directory component of `path`.
|
||||
|
||||
## Examples
|
||||
|
||||
Path.dirname("/foo/bar.ex")
|
||||
#=> "/foo"
|
||||
Path.dirname("/foo/bar/baz.ex")
|
||||
#=> "/foo/bar"
|
||||
|
||||
"""
|
||||
@spec dirname(t) :: binary
|
||||
def dirname(path) do
|
||||
FN.dirname(String.from_char_data!(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the extension of the last component of `path`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.extname("foo.erl")
|
||||
".erl"
|
||||
|
||||
iex> Path.extname("~/foo/bar")
|
||||
""
|
||||
|
||||
"""
|
||||
@spec extname(t) :: binary
|
||||
def extname(path) do
|
||||
FN.extension(String.from_char_data!(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `path` with the `extension` stripped.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.rootname("/foo/bar")
|
||||
"/foo/bar"
|
||||
|
||||
iex> Path.rootname("/foo/bar.ex")
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec rootname(t) :: binary
|
||||
def rootname(path) do
|
||||
FN.rootname(String.from_char_data!(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `path` with the `extension` stripped. This function should be used to
|
||||
remove a specific extension which might, or might not, be there.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.rootname("/foo/bar.erl", ".erl")
|
||||
"/foo/bar"
|
||||
|
||||
iex> Path.rootname("/foo/bar.erl", ".ex")
|
||||
"/foo/bar.erl"
|
||||
|
||||
"""
|
||||
@spec rootname(t, t) :: binary
|
||||
def rootname(path, extension) do
|
||||
FN.rootname(String.from_char_data!(path), String.from_char_data!(extension))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a string with one or more path components joined by the path separator.
|
||||
|
||||
This function should be used to convert a list of strings to a path.
|
||||
Note that any trailing slash is removed on join.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.join(["~", "foo"])
|
||||
"~/foo"
|
||||
|
||||
iex> Path.join(["foo"])
|
||||
"foo"
|
||||
|
||||
iex> Path.join(["/", "foo", "bar/"])
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec join([t]) :: binary
|
||||
def join([name1, name2|rest]), do:
|
||||
join([join(name1, name2)|rest])
|
||||
def join([name]), do:
|
||||
do_join(String.from_char_data!(name), <<>>, [], major_os_type())
|
||||
|
||||
@doc """
|
||||
Joins two paths.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.join("foo", "bar")
|
||||
"foo/bar"
|
||||
|
||||
"""
|
||||
@spec join(t, t) :: binary
|
||||
def join(left, right),
|
||||
do: do_join(String.from_char_data!(left), relative(right), [], major_os_type())
|
||||
|
||||
defp major_os_type do
|
||||
:os.type |> elem(0)
|
||||
end
|
||||
|
||||
defp do_join(<<uc_letter, ?:, rest :: binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
|
||||
do_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
|
||||
defp do_join(<<?\\, rest :: binary>>, relativename, result, :win32), do:
|
||||
do_join(<<?/, rest :: binary>>, relativename, result, :win32)
|
||||
defp do_join(<<?/, rest :: binary>>, relativename, [?., ?/|result], os_type), do:
|
||||
do_join(rest, relativename, [?/|result], os_type)
|
||||
defp do_join(<<?/, rest :: binary>>, relativename, [?/|result], os_type), do:
|
||||
do_join(rest, relativename, [?/|result], os_type)
|
||||
defp do_join(<<>>, <<>>, result, os_type), do:
|
||||
iodata_to_binary(maybe_remove_dirsep(result, os_type))
|
||||
defp do_join(<<>>, relativename, [?:|rest], :win32), do:
|
||||
do_join(relativename, <<>>, [?:|rest], :win32)
|
||||
defp do_join(<<>>, relativename, [?/|result], os_type), do:
|
||||
do_join(relativename, <<>>, [?/|result], os_type)
|
||||
defp do_join(<<>>, relativename, result, os_type), do:
|
||||
do_join(relativename, <<>>, [?/|result], os_type)
|
||||
defp do_join(<<char, rest :: binary>>, relativename, result, os_type), do:
|
||||
do_join(rest, relativename, [char|result], os_type)
|
||||
|
||||
defp maybe_remove_dirsep([?/, ?:, letter], :win32), do:
|
||||
[letter, ?:, ?/]
|
||||
defp maybe_remove_dirsep([?/], _), do:
|
||||
[?/]
|
||||
defp maybe_remove_dirsep([?/|name], _), do:
|
||||
:lists.reverse(name)
|
||||
defp maybe_remove_dirsep(name, _), do:
|
||||
:lists.reverse(name)
|
||||
|
||||
@doc """
|
||||
Returns a list with the path split by the path separator.
|
||||
If an empty string is given, returns the root path.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.split("")
|
||||
[]
|
||||
|
||||
iex> Path.split("foo")
|
||||
["foo"]
|
||||
|
||||
iex> Path.split("/foo/bar")
|
||||
["/", "foo", "bar"]
|
||||
|
||||
"""
|
||||
@spec split(t) :: [binary]
|
||||
|
||||
# Work around a bug in Erlang on UNIX
|
||||
def split(""), do: []
|
||||
|
||||
def split(path) do
|
||||
FN.split(String.from_char_data!(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Traverses paths according to the given `glob` expression.
|
||||
|
||||
The wildcard looks like an ordinary path, except that certain
|
||||
"wildcard characters" are interpreted in a special way. The
|
||||
following characters are special:
|
||||
|
||||
* `?` - Matches one character.
|
||||
* `*` - Matches any number of characters up to the end of
|
||||
the filename, the next dot, or the next slash.
|
||||
* `**` - Two adjacent <c>*</c>'s used as a single pattern will
|
||||
match all files and zero or more directories and subdirectories.
|
||||
* `[char1,char2,...]` - Matches any of the characters listed. Two characters
|
||||
separated by a hyphen will match a range of characters.
|
||||
* `{item1,item2,...}` - Matches one of the alternatives.
|
||||
|
||||
Other characters represent themselves. Only paths that have
|
||||
exactly the same character in the same position will match. Note
|
||||
that matching is case-sensitive; i.e. "a" will not match "A".
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine you have a directory called `projects` with three Elixir projects
|
||||
inside of it: `elixir`, `ex_doc` and `dynamo`. You can find all `.beam` files
|
||||
inside the `ebin` directory of each project as follows:
|
||||
|
||||
Path.wildcard("projects/*/ebin/**/*.beam")
|
||||
|
||||
If you want to search for both `.beam` and `.app` files, you could do:
|
||||
|
||||
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
|
||||
|
||||
"""
|
||||
@spec wildcard(t) :: [binary]
|
||||
def wildcard(glob) do
|
||||
glob
|
||||
|> List.from_char_data!
|
||||
|> :filelib.wildcard
|
||||
|> Enum.map(&String.from_char_data!/1)
|
||||
end
|
||||
|
||||
# Normalize the given path by expanding "..", "." and "~".
|
||||
|
||||
defp expand_home(type) do
|
||||
case String.from_char_data!(type) do
|
||||
"~" <> rest -> System.user_home! <> rest
|
||||
rest -> rest
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize(path), do: normalize(split(path), [])
|
||||
|
||||
defp normalize([".."|t], [_|acc]) do
|
||||
normalize t, acc
|
||||
end
|
||||
|
||||
defp normalize(["."|t], acc) do
|
||||
normalize t, acc
|
||||
end
|
||||
|
||||
defp normalize([h|t], acc) do
|
||||
normalize t, [h|acc]
|
||||
end
|
||||
|
||||
defp normalize([], acc) do
|
||||
join :lists.reverse(acc)
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
defmodule Port do
|
||||
@moduledoc """
|
||||
Functions related to Erlang ports.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#open_port-2.
|
||||
"""
|
||||
def open(name, settings) do
|
||||
:erlang.open_port(name, settings)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_close-1.
|
||||
"""
|
||||
def close(port) do
|
||||
:erlang.port_close(port)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_command-2.
|
||||
"""
|
||||
def command(port, data, options \\ []) do
|
||||
:erlang.port_command(port, data, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_connect-2.
|
||||
"""
|
||||
def connect(port, pid) do
|
||||
:erlang.port_connect(port, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_control-3.
|
||||
"""
|
||||
def control(port, operation, data) do
|
||||
:erlang.port_control(port, operation, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_call-3.
|
||||
"""
|
||||
def call(port, operation, data) do
|
||||
:erlang.port_call(port, operation, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_info-1.
|
||||
"""
|
||||
def info(port) do
|
||||
:erlang.port_info(port)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_info-2.
|
||||
"""
|
||||
def info(port, item) do
|
||||
:erlang.port_info(port, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#ports-0.
|
||||
"""
|
||||
def list do
|
||||
:erlang.ports
|
||||
end
|
||||
end
|
||||
@@ -1,430 +0,0 @@
|
||||
defmodule Process do
|
||||
@moduledoc """
|
||||
Conveniences for working with processes and the process dictionary.
|
||||
|
||||
Some of the functions in this module are inlined by the compiler,
|
||||
similar to functions in the `Kernel` module and they are explicitly
|
||||
marked in their docs as "inlined by the compiler". For more information
|
||||
about inlined functions, check out the `Kernel` module.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns true if the process exists and is alive, that is,
|
||||
is not exiting and has not exited. Otherwise, returns false.
|
||||
|
||||
`pid` must refer to a process at the local node.
|
||||
"""
|
||||
@spec alive?(pid) :: boolean
|
||||
def alive?(pid) do
|
||||
:erlang.is_process_alive(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all key-values in the dictionary.
|
||||
"""
|
||||
@spec get :: [{term, term}]
|
||||
def get do
|
||||
:erlang.get()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for the given key.
|
||||
"""
|
||||
@spec get(term) :: term
|
||||
@spec get(term, default :: term) :: term
|
||||
def get(key, default \\ nil) do
|
||||
case :erlang.get(key) do
|
||||
:undefined ->
|
||||
default
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all keys that have the given `value`.
|
||||
"""
|
||||
@spec get_keys(term) :: [term]
|
||||
def get_keys(value) do
|
||||
:erlang.get_keys(value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the given key-value in the process dictionary.
|
||||
"""
|
||||
@spec put(term, term) :: term | nil
|
||||
def put(key, value) do
|
||||
nillify :erlang.put(key, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all items in the dictionary.
|
||||
"""
|
||||
@spec delete :: [{term, term}]
|
||||
def delete() do
|
||||
IO.write :stderr, "Process.delete/0 is deprecated\n#{Exception.format_stacktrace}"
|
||||
:erlang.erase()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given key from the dictionary.
|
||||
"""
|
||||
@spec delete(term) :: term | nil
|
||||
def delete(key) do
|
||||
nillify :erlang.erase(key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an exit signal with the given reason to the pid.
|
||||
|
||||
The following behaviour applies if reason is any term except `:normal` or `:kill`:
|
||||
|
||||
1) If pid is not trapping exits, pid will exit with the given reason;
|
||||
|
||||
2) If pid is trapping exits, the exit signal is transformed into a message
|
||||
{:EXIT, from, reason} and delivered to the message queue of pid;
|
||||
|
||||
3) If reason is the atom `:normal`, pid will not exit. If it is trapping exits,
|
||||
the exit signal is transformed into a message {:EXIT, from, :normal} and
|
||||
delivered to its message queue;
|
||||
|
||||
4) If reason is the atom `:kill`, that is if `exit(pid, :kill)` is called, an
|
||||
untrappable exit signal is sent to pid which will unconditionally exit with
|
||||
exit reason `:killed`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
Process.exit(pid, :kill)
|
||||
|
||||
"""
|
||||
@spec exit(pid, term) :: true
|
||||
def exit(pid, reason) do
|
||||
:erlang.exit(pid, reason)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends a message to the given process.
|
||||
|
||||
If the option `:noconnect` is used and sending the message would require an
|
||||
auto-connection to another node the message is not sent and `:noconnect` is
|
||||
returned.
|
||||
|
||||
If the option `:nosuspend` is used and sending the message would cause the
|
||||
sender to be suspended the message is not sent and `:nosuspend` is returned.
|
||||
|
||||
Otherwise the message is sent and `:ok` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Process.send({:name, :node_does_not_exist}, :hi, [:noconnect])
|
||||
:noconnect
|
||||
|
||||
"""
|
||||
@spec send(dest, msg, [option]) :: result when
|
||||
dest: pid | port | atom | {atom, node},
|
||||
msg: any,
|
||||
option: :noconnect | :nosuspend,
|
||||
result: :ok | :noconnect | :nosuspend
|
||||
def send(dest, msg, options) do
|
||||
:erlang.send(dest, msg, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends `msg` to `dest` after `time` millisecons.
|
||||
|
||||
If `dest` is a pid, it has to be a pid of a local process, dead or alive.
|
||||
If `dest` is an atom, it is supposed to be the name of a registered process
|
||||
which is looked up at the time of delivery. No error is given if the name does
|
||||
not refer to a process.
|
||||
|
||||
This function returns a timer reference, which can be read or canceled with
|
||||
`:erlang.read_timer/1`, `:erlang.start_timer/3` and `:erlang.cancel_timer/1`.
|
||||
Note `time` cannot be greater than `4294967295`.
|
||||
|
||||
Finally, the timer will be automatically canceled if the given `dest` is a pid
|
||||
which is not alive or when the given pid exits. Note that timers will not be
|
||||
automatically canceled when `dest` is an atom (as the atom resolution is done
|
||||
on delivery).
|
||||
"""
|
||||
@spec send_after(pid | atom, term, non_neg_integer) :: reference
|
||||
def send_after(dest, msg, time) do
|
||||
:erlang.send_after(time, dest, msg)
|
||||
end
|
||||
|
||||
@type spawn_opt :: :link | :monitor | {:priority, :low | :normal | :high} |
|
||||
{:fullsweep_after, non_neg_integer} |
|
||||
{:min_heap_size, non_neg_integer} |
|
||||
{:min_bin_vheap_size, non_neg_integer}
|
||||
@type spawn_opts :: [spawn_opt]
|
||||
|
||||
@doc """
|
||||
Spawns the given module and function passing the given args
|
||||
according to the given options.
|
||||
|
||||
The result depends on the given options. In particular,
|
||||
if `:monitor` is given as an option, it will return a tuple
|
||||
containing the pid and the monitoring reference, otherwise
|
||||
just the spawned process pid.
|
||||
|
||||
It also accepts extra options, for the list of available options
|
||||
check http://www.erlang.org/doc/man/erlang.html#spawn_opt-4
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(fun, opts) do
|
||||
:erlang.spawn_opt(fun, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns the given module and function passing the given args
|
||||
according to the given options.
|
||||
|
||||
The result depends on the given options. In particular,
|
||||
if `:monitor` is given as an option, it will return a tuple
|
||||
containing the pid and the monitoring reference, otherwise
|
||||
just the spawned process pid.
|
||||
|
||||
It also accepts extra options, for the list of available options
|
||||
check http://www.erlang.org/doc/man/erlang.html#spawn_opt-4
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(module, atom, list, spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(mod, fun, args, opts) do
|
||||
:erlang.spawn_opt(mod, fun, args, opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def spawn(fun) do
|
||||
# IO.write :stderr, "Process.spawn/1 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.spawn(fun)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def spawn(mod, fun, args) do
|
||||
# IO.write :stderr, "Process.spawn/3 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.spawn(mod, fun, args)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def spawn_link(fun) do
|
||||
# IO.write :stderr, "Process.spawn_link/1 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.spawn_link(fun)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def spawn_link(mod, fun, args) do
|
||||
# IO.write :stderr, "Process.spawn_link/3 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.spawn_link(mod, fun, args)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def spawn_monitor(fun) do
|
||||
# IO.write :stderr, "Process.spawn_monitor/1 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.spawn_monitor(fun)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def spawn_monitor(mod, fun, args) do
|
||||
# IO.write :stderr, "Process.spawn_monitor/3 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.spawn_monitor(mod, fun, args)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def self() do
|
||||
# IO.write :stderr, "Process.self/0 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.self()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def send(dest, msg) do
|
||||
# IO.write :stderr, "Process.send/2 is deprecated, use the one in Kernel instead\n#{Exception.format_stacktrace}"
|
||||
:erlang.send(dest, msg)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The calling process starts monitoring the item given.
|
||||
It returns the monitor reference.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor-2 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec monitor(pid | {reg_name :: atom, node :: atom} | reg_name :: atom) :: reference
|
||||
def monitor(item) do
|
||||
:erlang.monitor(:process, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
If monitor_ref is a reference which the calling process
|
||||
obtained by calling monitor/1, this monitoring is turned off.
|
||||
If the monitoring is already turned off, nothing happens.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#demonitor-2 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec demonitor(reference) :: true
|
||||
@spec demonitor(reference, options :: [:flush | :info]) :: boolean
|
||||
def demonitor(monitor_ref, options \\ []) do
|
||||
:erlang.demonitor(monitor_ref, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of process identifiers corresponding to all the
|
||||
processes currently existing on the local node.
|
||||
|
||||
Note that a process that is exiting, exists but is not alive, i.e.,
|
||||
alive?/1 will return false for a process that is exiting,
|
||||
but its process identifier will be part of the result returned.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#processes-0 for more info.
|
||||
"""
|
||||
@spec list :: [pid]
|
||||
def list do
|
||||
:erlang.processes()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a link between the calling process and another process
|
||||
(or port) `pid`, if there is not such a link already.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#link-1 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec link(pid | port) :: true
|
||||
def link(pid) do
|
||||
:erlang.link(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the link, if there is one, between the calling process and
|
||||
the process or port referred to by `pid`. Returns true and does not
|
||||
fail, even if there is no link or `id` does not exist
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#unlink-1 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec unlink(pid | port) :: true
|
||||
def unlink(pid) do
|
||||
:erlang.unlink(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Associates the name with a pid or a port identifier. name, which must
|
||||
be an atom, can be used instead of the pid / port identifier with the
|
||||
`Kernel.send/2` function.
|
||||
|
||||
`Process.register/2` will fail with `ArgumentError` if the pid supplied
|
||||
is no longer alive, (check with `alive?/1`) or if the name is
|
||||
already registered (check with `registered?/1`).
|
||||
"""
|
||||
@spec register(pid | port, atom) :: true
|
||||
def register(pid, name) do
|
||||
:erlang.register(name, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the registered name, associated with a pid or a port identifier.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#unregister-1 for more info.
|
||||
"""
|
||||
@spec unregister(atom) :: true
|
||||
def unregister(name) do
|
||||
:erlang.unregister(name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid or port identifier with the registered name.
|
||||
Returns nil if the name is not registered.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#whereis-1 for more info.
|
||||
"""
|
||||
@spec whereis(atom) :: pid | port | nil
|
||||
def whereis(name) do
|
||||
nillify :erlang.whereis(name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of the group leader for the process which evaluates the function.
|
||||
"""
|
||||
@spec group_leader :: pid
|
||||
def group_leader do
|
||||
:erlang.group_leader
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the group leader of `pid` to `leader`. Typically, this is used when a processes
|
||||
started from a certain shell should have another group leader than `:init`.
|
||||
"""
|
||||
@spec group_leader(pid, leader :: pid) :: true
|
||||
def group_leader(pid, leader) do
|
||||
:erlang.group_leader(leader, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of names which have been registered using register/2.
|
||||
"""
|
||||
@spec registered :: [atom]
|
||||
def registered do
|
||||
:erlang.registered()
|
||||
end
|
||||
|
||||
@typep process_flag :: :trap_exit | :error_handler | :min_heap_size |
|
||||
:min_bin_vheap_size | :priority | :save_calls |
|
||||
:sensitive
|
||||
@doc """
|
||||
Sets certain flags for the process which calls this function.
|
||||
Returns the old value of the flag.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_flag-2 for more info.
|
||||
"""
|
||||
@spec flag(process_flag, term) :: term
|
||||
def flag(flag, value) do
|
||||
:erlang.process_flag(flag, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets certain flags for the process Pid, in the same manner as flag/2.
|
||||
Returns the old value of the flag. The allowed values for Flag are
|
||||
only a subset of those allowed in flag/2, namely: save_calls.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_flag-3 for more info.
|
||||
"""
|
||||
@spec flag(pid, process_flag, term) :: term
|
||||
def flag(pid, flag, value) do
|
||||
:erlang.process_flag(pid, flag, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the process identified by pid or nil if the process
|
||||
is not alive.
|
||||
Use this only for debugging information.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_info-1 for more info.
|
||||
"""
|
||||
@spec info(pid) :: Keyword.t
|
||||
def info(pid) do
|
||||
nillify :erlang.process_info(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the process identified by pid
|
||||
or nil if the process is not alive.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_info-2 for more info.
|
||||
"""
|
||||
@spec info(pid, atom) :: {atom, term}
|
||||
def info(pid, spec) do
|
||||
nillify :erlang.process_info(pid, spec)
|
||||
end
|
||||
|
||||
@compile {:inline, nillify: 1}
|
||||
defp nillify(:undefined), do: nil
|
||||
defp nillify(other), do: other
|
||||
end
|
||||
@@ -1,279 +0,0 @@
|
||||
defmodule Protocol do
|
||||
@moduledoc false
|
||||
|
||||
# Callback for defprotocol.
|
||||
@doc false
|
||||
def defprotocol(name, [do: block]) do
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel, except: [
|
||||
defmacrop: 1, defmacrop: 2, defmacro: 1, defmacro: 2,
|
||||
defp: 1, defp: 2, def: 1, def: 2
|
||||
]
|
||||
|
||||
# Import the new dsl that holds the new def
|
||||
import Protocol.DSL, only: :macros
|
||||
|
||||
# Compile with debug info for consolidation
|
||||
@compile :debug_info
|
||||
|
||||
# Set up a clear slate to store defined functions
|
||||
@functions []
|
||||
@fallback_to_any false
|
||||
|
||||
# Invoke the user given block
|
||||
unquote(block)
|
||||
|
||||
# Finalize expansion
|
||||
unquote(after_defprotocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp after_defprotocol do
|
||||
quote unquote: false do
|
||||
# == Deprecated records handling ==
|
||||
{arg, impl} = Protocol.rec_impl_for(__MODULE__)
|
||||
Kernel.def impl_for(unquote(arg)) when Kernel.is_record(unquote(arg)), do: unquote(impl)
|
||||
# == Deprecated records handling ==
|
||||
|
||||
@spec impl_for(term) :: module | nil
|
||||
Kernel.def impl_for(data)
|
||||
|
||||
# Define the implementation for structs.
|
||||
#
|
||||
# It simply delegates to struct_impl_for which is then
|
||||
# optimized during protocol consolidation.
|
||||
Kernel.def impl_for(%{__struct__: struct}) when :erlang.is_atom(struct) do
|
||||
struct_impl_for(struct)
|
||||
end
|
||||
|
||||
# Define the implementation for builtins.
|
||||
for {guard, mod} <- Protocol.builtin do
|
||||
target = Module.concat(__MODULE__, mod)
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case impl_for?(unquote(target)) do
|
||||
true -> unquote(target).__impl__(:name)
|
||||
false -> any_impl_for
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec impl_for!(term) :: module | no_return
|
||||
Kernel.def impl_for!(data) do
|
||||
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
|
||||
end
|
||||
|
||||
# Internal handler for Any
|
||||
if @fallback_to_any do
|
||||
Kernel.defp any_impl_for do
|
||||
case impl_for?(__MODULE__.Any) do
|
||||
true -> __MODULE__.Any.__impl__(:name)
|
||||
false -> nil
|
||||
end
|
||||
end
|
||||
else
|
||||
Kernel.defp any_impl_for, do: nil
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
target = Module.concat(__MODULE__, struct)
|
||||
case impl_for?(target) do
|
||||
true -> target.__impl__(:name)
|
||||
false -> any_impl_for
|
||||
end
|
||||
end
|
||||
|
||||
# Check if compilation is available internally
|
||||
Kernel.defp impl_for?(target) do
|
||||
Code.ensure_compiled?(target) and
|
||||
function_exported?(target, :__impl__, 1)
|
||||
end
|
||||
|
||||
# Inline any and struct implementations
|
||||
@compile {:inline, any_impl_for: 0, struct_impl_for: 1, impl_for?: 1}
|
||||
|
||||
if :code.ensure_loaded(Kernel.Typespec) == {:module, Kernel.Typespec} and
|
||||
not Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
|
||||
@type t :: term
|
||||
end
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :protocol, persist: true)
|
||||
@protocol [fallback_to_any: !!@fallback_to_any, consolidated: false]
|
||||
|
||||
@doc false
|
||||
Kernel.def __protocol__(:name), do: __MODULE__
|
||||
Kernel.def __protocol__(:functions), do: unquote(:lists.sort(@functions))
|
||||
end
|
||||
end
|
||||
|
||||
# Callback for defimpl.
|
||||
@doc false
|
||||
def defimpl(protocol, opts) do
|
||||
do_defimpl(protocol, :lists.keysort(1, opts))
|
||||
end
|
||||
|
||||
defp do_defimpl(protocol, [do: block, for: for]) when is_list(for) do
|
||||
for f <- for, do: do_defimpl(protocol, [do: block, for: f])
|
||||
end
|
||||
|
||||
defp do_defimpl(protocol, [do: block, for: for]) do
|
||||
quote do
|
||||
protocol = unquote(protocol)
|
||||
for = unquote(for)
|
||||
name = Module.concat(protocol, for)
|
||||
|
||||
Protocol.assert_protocol(protocol)
|
||||
|
||||
defmodule name do
|
||||
@behaviour unquote(protocol)
|
||||
@protocol unquote(protocol)
|
||||
@for unquote(for)
|
||||
|
||||
unquote(block)
|
||||
|
||||
Module.register_attribute(__MODULE__, :impl, persist: true)
|
||||
@impl [protocol: @protocol, for: @for]
|
||||
|
||||
@doc false
|
||||
def __impl__(:name), do: __MODULE__
|
||||
def __impl__(:protocol), do: @protocol
|
||||
def __impl__(:for), do: @for
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check if the given module is a protocol. Raises an error
|
||||
# if not loaded or not a protocol.
|
||||
@doc false
|
||||
def assert_protocol(module) do
|
||||
case Code.ensure_compiled(module) do
|
||||
{:module, ^module} -> nil
|
||||
_ -> raise ArgumentError, message: "#{inspect module} is not loaded"
|
||||
end
|
||||
|
||||
try do
|
||||
module.__protocol__(:name)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
raise ArgumentError, message: "#{inspect module} is not a protocol"
|
||||
end
|
||||
end
|
||||
|
||||
# Builtin types.
|
||||
@doc false
|
||||
def builtin do
|
||||
[is_tuple: Tuple,
|
||||
is_atom: Atom,
|
||||
is_list: List,
|
||||
is_map: Map,
|
||||
is_bitstring: BitString,
|
||||
is_integer: Integer,
|
||||
is_float: Float,
|
||||
is_function: Function,
|
||||
is_pid: PID,
|
||||
is_port: Port,
|
||||
is_reference: Reference]
|
||||
end
|
||||
|
||||
# Implements the function that detects the protocol and
|
||||
# returns the module to dispatch to.
|
||||
@doc false
|
||||
def rec_impl_for(current) do
|
||||
all = [Any] ++ for {_guard, mod} <- builtin, do: mod
|
||||
arg = quote do: arg
|
||||
target = Module.concat(current, Tuple)
|
||||
|
||||
fallback = quote do
|
||||
case impl_for?(unquote(target)) do
|
||||
true -> unquote(target).__impl__(:name)
|
||||
false -> any_impl_for
|
||||
end
|
||||
end
|
||||
|
||||
impl_for = quote do
|
||||
atom = :erlang.element(1, unquote(arg))
|
||||
|
||||
case not(atom in unquote(all)) and match?('Elixir.' ++ _, atom_to_list(atom)) do
|
||||
true ->
|
||||
target = Module.concat(unquote(current), atom)
|
||||
case impl_for?(target) do
|
||||
true -> target.__impl__(:name)
|
||||
false -> unquote(fallback)
|
||||
end
|
||||
false ->
|
||||
unquote(fallback)
|
||||
end
|
||||
end
|
||||
|
||||
{arg, impl_for}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Protocol.DSL do
|
||||
@moduledoc false
|
||||
|
||||
@doc false
|
||||
defmacro def({_, _, args}) when args == [] or is_atom(args) do
|
||||
raise ArgumentError, message: "protocol functions expect at least one argument"
|
||||
end
|
||||
|
||||
defmacro def({name, _, args}) when is_atom(name) and is_list(args) do
|
||||
arity = length(args)
|
||||
|
||||
type_args = for _ <- :lists.seq(2, arity), do: quote(do: term)
|
||||
type_args = [quote(do: t) | type_args]
|
||||
|
||||
call_args = for i <- :lists.seq(2, arity),
|
||||
do: {binary_to_atom(<<?x, i + 64>>), [], __MODULE__}
|
||||
call_args = [quote(do: t) | call_args]
|
||||
|
||||
quote do
|
||||
name = unquote(name)
|
||||
arity = unquote(arity)
|
||||
|
||||
@functions [{name, arity}|@functions]
|
||||
|
||||
# Generate a fake definition with the user
|
||||
# signature that will be used by docs
|
||||
Kernel.def unquote(name)(unquote_splicing(args))
|
||||
|
||||
# Generate the actual implementation
|
||||
Kernel.def unquote(name)(unquote_splicing(call_args)) do
|
||||
impl_for!(t).unquote(name)(unquote_splicing(call_args))
|
||||
end
|
||||
|
||||
# Convert the spec to callback if possible,
|
||||
# otherwise generate a dummy callback
|
||||
Protocol.DSL.__spec__?(__MODULE__, name, arity) ||
|
||||
@callback unquote(name)(unquote_splicing(type_args)) :: term
|
||||
end
|
||||
end
|
||||
|
||||
defmacro def(_) do
|
||||
raise ArgumentError, message: "invalid args for def inside defprotocol"
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __spec__?(module, name, arity) do
|
||||
case :code.ensure_loaded(Kernel.Typespec) do
|
||||
{:module, Kernel.Typespec} ->
|
||||
tuple = {name, arity}
|
||||
specs = Module.get_attribute(module, :spec)
|
||||
|
||||
found = for {k, v} <- specs, k == tuple do
|
||||
Kernel.Typespec.define_callback(module, tuple, v)
|
||||
true
|
||||
end
|
||||
|
||||
found != []
|
||||
{:error, _} ->
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,249 +0,0 @@
|
||||
defmodule Protocol.Consolidation do
|
||||
@moduledoc """
|
||||
Module responsible for consolidating protocols and helpers for
|
||||
extracting protocols and implementations from code paths for
|
||||
consolidation.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Extract all protocols from the given paths.
|
||||
|
||||
The paths can be either a char list or a string. Internally
|
||||
they are worked on as char lists, so passing them as lists
|
||||
avoid extra conversion.
|
||||
|
||||
## Examples
|
||||
|
||||
# Get Elixir's ebin and retrieve all protocols
|
||||
iex> path = :code.lib_dir(:elixir, :ebin)
|
||||
iex> mods = Protocol.Consolidation.extract_protocols([path])
|
||||
iex> Enumerable in mods
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_protocols([char_list | String.t]) :: [atom]
|
||||
def extract_protocols(paths) do
|
||||
extract_matching_by_attribute paths, 'Elixir.',
|
||||
fn module, attributes ->
|
||||
case attributes[:protocol] do
|
||||
[fallback_to_any: _, consolidated: _] -> module
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extract all types implemented for the given protocol from
|
||||
the given paths.
|
||||
|
||||
The paths can be either a char list or a string. Internally
|
||||
they are worked on as char lists, so passing them as lists
|
||||
avoid extra conversion.
|
||||
|
||||
## Examples
|
||||
|
||||
# Get Elixir's ebin and retrieve all protocols
|
||||
iex> path = :code.lib_dir(:elixir, :ebin)
|
||||
iex> mods = Protocol.Consolidation.extract_impls(Enumerable, [path])
|
||||
iex> List in mods
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_impls(module, [char_list | String.t]) :: [atom]
|
||||
def extract_impls(protocol, paths) when is_atom(protocol) do
|
||||
prefix = atom_to_list(protocol) ++ '.'
|
||||
extract_matching_by_attribute paths, prefix, fn
|
||||
_mod, attributes ->
|
||||
case attributes[:impl] do
|
||||
[protocol: ^protocol, for: for] -> for
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
file <- list_dir(path),
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_list(path) do
|
||||
case :file.list_dir(path) do
|
||||
{:ok, files} -> files
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp list_dir(path), do: list_dir(to_char_list(path))
|
||||
|
||||
defp extract_from_file(path, file, prefix, callback) do
|
||||
if :lists.prefix(prefix, file) and :filename.extension(file) == '.beam' do
|
||||
extract_from_beam(:filename.join(path, file), callback)
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_from_beam(file, callback) do
|
||||
case :beam_lib.chunks(file, [:attributes]) do
|
||||
{:ok, {module, [attributes: attributes]}} ->
|
||||
callback.(module, attributes)
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop if_ok(expr, call) do
|
||||
quote do
|
||||
case unquote(expr) do
|
||||
{:ok, var} -> unquote(Macro.pipe(quote(do: var), call, 0))
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a protocol and a list of implementations and
|
||||
consolidates the given protocol. Consolidation happens
|
||||
by changing the protocol `impl_for` in the abstract
|
||||
format to have fast lookup rules.
|
||||
|
||||
It returns the updated version of the protocol bytecode.
|
||||
A given bytecode or protocol implementation can be checked
|
||||
to be consolidated or not by analyzing the protocol
|
||||
attribute:
|
||||
|
||||
Enumerable.__info__(:attributes)[:protocol]
|
||||
|
||||
If the first element of the tuple is true, it means
|
||||
the protocol was consolidated.
|
||||
|
||||
This function does not load the protocol at any point
|
||||
nor loads the new bytecode for the compiled module.
|
||||
"""
|
||||
@spec apply_to(module, [module]) ::
|
||||
{:ok, binary} |
|
||||
{:error, :not_a_protocol} |
|
||||
{:error, :no_beam_info}
|
||||
def apply_to(protocol, types) when is_atom(protocol) do
|
||||
raise ArgumentError, message: "consolidation is disabled as we can't consolidate records " <>
|
||||
"and structs at once. Consolidation will be added back once " <>
|
||||
"polymorphic records are removed"
|
||||
ensure_protocol(protocol)
|
||||
|> if_ok(change_debug_info types)
|
||||
|> if_ok(compile)
|
||||
end
|
||||
|
||||
# Ensure the given module is loaded and is a protocol.
|
||||
defp ensure_protocol(protocol) do
|
||||
case :beam_lib.chunks(beam_file(protocol), [:abstract_code, :attributes]) do
|
||||
{:ok, {^protocol, [abstract_code: {_raw, abstract_code},
|
||||
attributes: attributes]}} ->
|
||||
case attributes[:protocol] do
|
||||
[fallback_to_any: any, consolidated: _] ->
|
||||
{:ok, {protocol, any, abstract_code}}
|
||||
_ ->
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
_ ->
|
||||
{:error, :no_beam_info}
|
||||
end
|
||||
end
|
||||
|
||||
defp beam_file(module) when is_atom(module) do
|
||||
case :code.which(module) do
|
||||
:non_existing -> module
|
||||
file -> file
|
||||
end
|
||||
end
|
||||
|
||||
# Change the debug information to the optimized
|
||||
# impl_for/1 dispatch version.
|
||||
defp change_debug_info({protocol, any, code}, types) do
|
||||
types = if any, do: types, else: List.delete(types, Any)
|
||||
all = [Any] ++ for {_guard, mod} <- Protocol.builtin, do: mod
|
||||
structs = types -- all
|
||||
change_impl_for(code, protocol, types, structs, false, [])
|
||||
end
|
||||
|
||||
defp change_impl_for([{:attribute, line, :protocol, opts}|t], protocol, types, structs, _, acc) do
|
||||
opts = [fallback_to_any: opts[:fallback_to_any], consolidated: true]
|
||||
change_impl_for(t, protocol, types, structs, true,
|
||||
[{:attribute, line, :protocol, opts}|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([{:function, line, :impl_for, 1, _}|t], protocol, types, structs, is_protocol, acc) do
|
||||
fallback = if Any in types, do: Module.concat(protocol, Any), else: nil
|
||||
|
||||
clauses = for {guard, mod} <- Protocol.builtin,
|
||||
mod in types,
|
||||
do: builtin_clause_for(mod, guard, protocol, line)
|
||||
|
||||
clauses = [struct_clause_for(line)|clauses] ++
|
||||
[fallback_clause_for(fallback, protocol, line)]
|
||||
|
||||
change_impl_for(t, protocol, types, structs, is_protocol,
|
||||
[{:function, line, :impl_for, 1, clauses}|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([{:function, line, :struct_impl_for, 1, _}|t], protocol, types, structs, is_protocol, acc) do
|
||||
fallback = if Any in types, do: Module.concat(protocol, Any), else: nil
|
||||
clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, line)
|
||||
clauses = clauses ++ [fallback_clause_for(fallback, protocol, line)]
|
||||
|
||||
change_impl_for(t, protocol, types, structs, is_protocol,
|
||||
[{:function, line, :struct_impl_for, 1, clauses}|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([h|t], protocol, info, types, is_protocol, acc) do
|
||||
change_impl_for(t, protocol, info, types, is_protocol, [h|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([], protocol, _info, _types, is_protocol, acc) do
|
||||
if is_protocol do
|
||||
{:ok, {protocol, Enum.reverse(acc)}}
|
||||
else
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
end
|
||||
|
||||
defp builtin_clause_for(mod, guard, protocol, line) do
|
||||
{:clause, line,
|
||||
[{:var, line, :x}],
|
||||
[[{:call, line,
|
||||
{:remote, line, {:atom, line, :erlang}, {:atom, line, guard}},
|
||||
[{:var, line, :x}],
|
||||
}]],
|
||||
[{:atom, line, Module.concat(protocol, mod)}]}
|
||||
end
|
||||
|
||||
defp struct_clause_for(line) do
|
||||
{:clause, line,
|
||||
[{:map, line, [
|
||||
{:map_field_exact, line, {:atom, line, :__struct__}, {:var, line, :x}}
|
||||
]}],
|
||||
[[{:call, line,
|
||||
{:remote, line, {:atom, line, :erlang}, {:atom, line, :is_atom}},
|
||||
[{:var, line, :x}],
|
||||
}]],
|
||||
[{:call, line,
|
||||
{:atom, line, :struct_impl_for},
|
||||
[{:var, line, :x}]}]}
|
||||
end
|
||||
|
||||
defp each_struct_clause_for(other, protocol, line) do
|
||||
{:clause, line, [{:atom, line, other}], [],
|
||||
[{:atom, line, Module.concat(protocol, other)}]}
|
||||
end
|
||||
|
||||
defp fallback_clause_for(value, _protocol, line) do
|
||||
{:clause, line, [{:var, line, :_}], [],
|
||||
[{:atom, line, value}]}
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
defp compile({protocol, code}) do
|
||||
opts = if Code.compiler_options[:debug_info], do: [:debug_info], else: []
|
||||
{:ok, ^protocol, binary, _warnings} = :compile.forms(code, [:return|opts])
|
||||
{:ok, binary}
|
||||
end
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
defmodule Range do
|
||||
@moduledoc """
|
||||
Defines a Range.
|
||||
|
||||
A Range are represented internally as a struct. However,
|
||||
the most common form of creating and matching on ranges
|
||||
is via the `../2` macro, auto-imported from Kernel:
|
||||
|
||||
iex> range = 1..3
|
||||
1..3
|
||||
iex> first .. last = range
|
||||
iex> first
|
||||
1
|
||||
iex> last
|
||||
3
|
||||
|
||||
"""
|
||||
|
||||
defstruct first: nil, last: nil
|
||||
|
||||
@type t(first, last) :: %{__struct__: Range, first: first, last: last}
|
||||
|
||||
@doc """
|
||||
Creates a new range.
|
||||
"""
|
||||
def new(first, last) do
|
||||
%Range{first: first, last: last}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument is a range.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.range?(1..3)
|
||||
true
|
||||
|
||||
iex> Range.range?(0)
|
||||
false
|
||||
|
||||
"""
|
||||
def range?(%Range{}), do: true
|
||||
def range?(_), do: false
|
||||
end
|
||||
|
||||
defprotocol Range.Iterator do
|
||||
@moduledoc """
|
||||
A protocol used for iterating range elements.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the function that calculates the next item.
|
||||
"""
|
||||
def next(first, range)
|
||||
|
||||
@doc """
|
||||
Count how many items are in the range.
|
||||
"""
|
||||
def count(first, range)
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: Range do
|
||||
def reduce(first .. last = range, acc, fun) do
|
||||
reduce(first, last, acc, fun, Range.Iterator.next(first, range), last >= first)
|
||||
end
|
||||
|
||||
defp reduce(_x, _y, {:halt, acc}, _fun, _next, _up) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp reduce(x, y, {:suspend, acc}, fun, next, up) do
|
||||
{:suspended, acc, &reduce(x, y, &1, fun, next, up)}
|
||||
end
|
||||
|
||||
defp reduce(x, y, {:cont, acc}, fun, next, true) when x <= y do
|
||||
reduce(next.(x), y, fun.(x, acc), fun, next, true)
|
||||
end
|
||||
|
||||
defp reduce(x, y, {:cont, acc}, fun, next, false) when x >= y do
|
||||
reduce(next.(x), y, fun.(x, acc), fun, next, false)
|
||||
end
|
||||
|
||||
defp reduce(_, _, {:cont, acc}, _fun, _next, _up) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
def member?(first .. last, value) do
|
||||
if first <= last do
|
||||
{:ok, first <= value and value <= last}
|
||||
else
|
||||
{:ok, last <= value and value <= first}
|
||||
end
|
||||
end
|
||||
|
||||
def count(first .. _ = range) do
|
||||
{:ok, Range.Iterator.count(first, range)}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Range.Iterator, for: Integer do
|
||||
def next(first, _ .. last) when is_integer(last) do
|
||||
if last >= first do
|
||||
&(&1 + 1)
|
||||
else
|
||||
&(&1 - 1)
|
||||
end
|
||||
end
|
||||
|
||||
def count(first, _ .. last) when is_integer(last) do
|
||||
if last >= first do
|
||||
last - first + 1
|
||||
else
|
||||
first - last + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Range do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(first .. last, opts) do
|
||||
concat [to_doc(first, opts), "..", to_doc(last, opts)]
|
||||
end
|
||||
end
|
||||
@@ -1,189 +0,0 @@
|
||||
defmodule Record do
|
||||
@moduledoc """
|
||||
Module to work, define and import records.
|
||||
|
||||
Records are simply tuples where the first element is an atom:
|
||||
|
||||
iex> Record.record? {User, "jose", 27}
|
||||
true
|
||||
|
||||
This module provides conveniences for working with records at
|
||||
compilation time, where compile-time field names are used to
|
||||
manipulate the tuples, providing fast operations on top of
|
||||
the tuples compact structure.
|
||||
|
||||
In Elixir, records are used in two situations:
|
||||
|
||||
1. To work with short, internal data. See `Inspect.Algebra`
|
||||
implementation for a good example;
|
||||
|
||||
2. To interface with Erlang records;
|
||||
|
||||
The macros `defrecord/3` and `defrecordp/3` can be used to create
|
||||
records while `extract/2` can be used to extract records from Erlang
|
||||
files.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Extracts record information from an Erlang file.
|
||||
|
||||
Returns a quoted expression containing the fields as a list
|
||||
of tuples. It expects the record name to be an atom and the
|
||||
library path to be a string at expansion time.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
|
||||
[size: :undefined, type: :undefined, access: :undefined, atime: :undefined,
|
||||
mtime: :undefined, ctime: :undefined, mode: :undefined, links: :undefined,
|
||||
major_device: :undefined, minor_device: :undefined, inode: :undefined,
|
||||
uid: :undefined, gid: :undefined]
|
||||
|
||||
"""
|
||||
defmacro extract(name, opts) when is_atom(name) and is_list(opts) do
|
||||
Macro.escape Record.Extractor.extract(name, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given `data` is a record of `kind`.
|
||||
|
||||
This is implemented as a macro so it can be used in guard clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> record = {User, "jose", 27}
|
||||
iex> Record.record?(record, User)
|
||||
true
|
||||
|
||||
"""
|
||||
defmacro record?(data, kind) do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
quote do
|
||||
is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
|
||||
and :erlang.element(1, unquote(data)) == unquote(kind)
|
||||
end
|
||||
false ->
|
||||
quote do
|
||||
result = unquote(data)
|
||||
is_tuple(result) and tuple_size(result) > 0
|
||||
and :erlang.element(1, result) == unquote(kind)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given `data` is a record.
|
||||
|
||||
This is implemented as a macro so it can be used in guard clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> record = {User, "jose", 27}
|
||||
iex> Record.record?(record)
|
||||
true
|
||||
iex> tuple = {}
|
||||
iex> Record.record?(tuple)
|
||||
false
|
||||
|
||||
"""
|
||||
defmacro record?(data) do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
quote do
|
||||
is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
|
||||
and is_atom(:erlang.element(1, unquote(data)))
|
||||
end
|
||||
false ->
|
||||
quote do
|
||||
result = unquote(data)
|
||||
is_tuple(result) and tuple_size(result) > 0
|
||||
and is_atom(:erlang.element(1, result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def defmacros(name, values, env, tag \\ nil) do
|
||||
Record.Deprecated.defmacros(name, values, env, tag)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def deftypes(values, types, env) do
|
||||
Record.Deprecated.deftypes(values, types, env)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def deffunctions(values, env) do
|
||||
Record.Deprecated.deffunctions(values, env)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a set of macros to create and access a record.
|
||||
|
||||
The macros are going to have `name`, a tag (which defaults)
|
||||
to the name if none is given, and a set of fields given by
|
||||
`kv`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule User do
|
||||
Record.defrecord :user, [name: "José", age: "25"]
|
||||
end
|
||||
|
||||
In the example above, a set of macros named `user` but with different
|
||||
arities will be defined to manipulate the underlying record:
|
||||
|
||||
# To create records
|
||||
user() #=> {:user, "José", 25}
|
||||
user(age: 26) #=> {:user, "José", 26}
|
||||
|
||||
# To get a field from the record
|
||||
user(record, :name) #=> "José"
|
||||
|
||||
# To update the record
|
||||
user(record, age: 26) #=> {:user, "José", 26}
|
||||
|
||||
By default, Elixir uses the record name as the first element of
|
||||
the tuple (the tag). But it can be changed to something else:
|
||||
|
||||
defmodule User do
|
||||
Record.defrecord :user, User, name: nil
|
||||
end
|
||||
|
||||
user() #=> {User, nil}
|
||||
|
||||
"""
|
||||
defmacro defrecord(name, tag \\ nil, kv) do
|
||||
quote bind_quoted: [name: name, tag: tag, kv: kv] do
|
||||
tag = tag || name
|
||||
fields = Macro.escape Record.Backend.default_fields(:record, kv)
|
||||
|
||||
defmacro(unquote(name)(args \\ [])) do
|
||||
Record.Backend.access(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
|
||||
defmacro(unquote(name)(record, args)) do
|
||||
Record.Backend.access(unquote(tag), unquote(fields), record, args, __CALLER__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `defrecord/3` but generates private macros.
|
||||
"""
|
||||
defmacro defrecordp(name, tag \\ nil, kv) do
|
||||
quote bind_quoted: [name: name, tag: tag, kv: kv] do
|
||||
tag = tag || name
|
||||
fields = Macro.escape Record.Backend.default_fields(:record, kv)
|
||||
|
||||
defmacrop(unquote(name)(args \\ [])) do
|
||||
Record.Backend.access(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
|
||||
defmacrop(unquote(name)(record, args)) do
|
||||
Record.Backend.access(unquote(tag), unquote(fields), record, args, __CALLER__)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,153 +0,0 @@
|
||||
defmodule Record.Backend do
|
||||
# Callback functions invoked by defrecord, defrecordp and friends.
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Normalizes a list of record or struct fields to have default values.
|
||||
"""
|
||||
def default_fields(type, fields) do
|
||||
Enum.map(fields, fn
|
||||
{ key, _ } = pair when is_atom(key) -> pair
|
||||
key when is_atom(key) -> { key, nil }
|
||||
other -> raise ArgumentError, message: "#{type} fields must be atoms, got: #{inspect other}"
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits a keywords list into fields and types.
|
||||
|
||||
This logic is shared by records and structs.
|
||||
"""
|
||||
def split_fields_and_types(kv) do
|
||||
if Keyword.keyword?(kv) do
|
||||
split_fields_and_types(kv, [], [])
|
||||
else
|
||||
{kv, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp split_fields_and_types([{field, {:::, _, [default, type]}}|t], fields, types) do
|
||||
split_fields_and_types(t, [{field, default}|fields], [{field, type}|types])
|
||||
end
|
||||
|
||||
defp split_fields_and_types([{field, default}|t], fields, types) do
|
||||
split_fields_and_types(t, [{field, default}|fields], [{field, quote(do: term)}|types])
|
||||
end
|
||||
|
||||
defp split_fields_and_types([field|t], fields, types) do
|
||||
split_fields_and_types(t, [field|fields], [{field, quote(do: term)}|types])
|
||||
end
|
||||
|
||||
defp split_fields_and_types([], fields, types) do
|
||||
{:lists.reverse(fields), :lists.reverse(types)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Callback invoked from record/0 and record/1 macros.
|
||||
"""
|
||||
def access(atom, fields, args, caller) do
|
||||
cond do
|
||||
is_atom(args) ->
|
||||
index(atom, fields, args)
|
||||
Keyword.keyword?(args) ->
|
||||
create(atom, fields, args, caller)
|
||||
true ->
|
||||
raise ArgumentError,
|
||||
message: "expected arguments to be a compile time atom or keywords, got: #{Macro.to_string args}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Callback invoked from the record/2 macro.
|
||||
"""
|
||||
def access(atom, fields, record, args, caller) do
|
||||
cond do
|
||||
is_atom(args) ->
|
||||
get(atom, fields, record, args)
|
||||
Keyword.keyword?(args) ->
|
||||
update(atom, fields, record, args, caller)
|
||||
true ->
|
||||
raise ArgumentError,
|
||||
message: "expected arguments to be a compile time atom or keywords, got: #{Macro.to_string args}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the index of field.
|
||||
"""
|
||||
def index(atom, fields, field) do
|
||||
if index = find_index(fields, field, 0) do
|
||||
index - 1 # Convert to Elixir index
|
||||
else
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect field}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new record with the given default fields and keyword values.
|
||||
"""
|
||||
def create(atom, fields, keyword, caller) do
|
||||
in_match = Macro.Env.in_match?(caller)
|
||||
|
||||
{match, remaining} =
|
||||
Enum.map_reduce(fields, keyword, fn({field, default}, each_keyword) ->
|
||||
new_fields =
|
||||
case Keyword.has_key?(each_keyword, field) do
|
||||
true -> Keyword.get(each_keyword, field)
|
||||
false ->
|
||||
case in_match do
|
||||
true -> {:_, [], nil}
|
||||
false -> Macro.escape(default)
|
||||
end
|
||||
end
|
||||
|
||||
{new_fields, Keyword.delete(each_keyword, field)}
|
||||
end)
|
||||
|
||||
case remaining do
|
||||
[] ->
|
||||
{:{}, [], [atom|match]}
|
||||
_ ->
|
||||
keys = for {key, _} <- remaining, do: key
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect hd(keys)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a record given by var with the given keyword.
|
||||
"""
|
||||
def update(atom, fields, var, keyword, caller) do
|
||||
if Macro.Env.in_match?(caller) do
|
||||
raise ArgumentError, message: "cannot invoke update style macro inside match"
|
||||
end
|
||||
|
||||
Enum.reduce keyword, var, fn({key, value}, acc) ->
|
||||
index = find_index(fields, key, 0)
|
||||
if index do
|
||||
quote do
|
||||
:erlang.setelement(unquote(index), unquote(acc), unquote(value))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a record key from the given var.
|
||||
"""
|
||||
def get(atom, fields, var, key) do
|
||||
index = find_index(fields, key, 0)
|
||||
if index do
|
||||
quote do
|
||||
:erlang.element(unquote(index), unquote(var))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect key}"
|
||||
end
|
||||
end
|
||||
|
||||
defp find_index([{k, _}|_], k, i), do: i + 2
|
||||
defp find_index([{_, _}|t], k, i), do: find_index(t, k, i + 1)
|
||||
defp find_index([], _k, _i), do: nil
|
||||
end
|
||||
@@ -1,526 +0,0 @@
|
||||
defmodule Record.Deprecated do
|
||||
@moduledoc false
|
||||
|
||||
def defrecord(name, fields, opts) do
|
||||
block = Keyword.get(opts, :do, nil)
|
||||
record_check!(fields)
|
||||
|
||||
quote do
|
||||
unquoted_fields = unquote(fields)
|
||||
|
||||
defmodule unquote(name) do
|
||||
import Record.DSL
|
||||
|
||||
@record_fields []
|
||||
@record_types []
|
||||
|
||||
Record.Deprecated.deffunctions(unquoted_fields, __ENV__)
|
||||
value = unquote(block)
|
||||
Record.Deprecated.deftypes(@record_fields, @record_types, __ENV__)
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp record_check!([{field, {:::, _, [_, _]}}|_]) when is_atom(field) do
|
||||
raise ArgumentError, message: "typespecs are not supported inlined with defrecord, " <>
|
||||
"please use record_type instead"
|
||||
end
|
||||
|
||||
defp record_check!([_|t]), do: record_check!(t)
|
||||
defp record_check!(_), do: :ok
|
||||
|
||||
def defrecordp(name, tag, fields) do
|
||||
case recordp_split(fields, [], [], false) do
|
||||
{:ok, fields, types, def_type} ->
|
||||
types = Macro.escape(types)
|
||||
|
||||
# bind_quoted isn't available when bootstrapping record
|
||||
quoted = quote [unquote: false] do
|
||||
Record.Deprecated.defmacros(name, fields, __ENV__, tag)
|
||||
|
||||
if def_type do
|
||||
type = binary_to_atom(atom_to_binary(name) <> "_t")
|
||||
@typep unquote(type)() :: {unquote(tag || name), unquote_splicing(types)}
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
def_type = unquote(def_type)
|
||||
fields = unquote(fields)
|
||||
types = unquote(types)
|
||||
tag = unquote(tag)
|
||||
name = unquote(name)
|
||||
unquote(quoted)
|
||||
end
|
||||
|
||||
:error ->
|
||||
quote do
|
||||
name = unquote(name)
|
||||
Record.Deprecated.defmacros(name, unquote(fields), __ENV__, unquote(tag))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recordp_split([{field, {:::, _, [default, type]}}|t], defaults, types, _) do
|
||||
recordp_split(t, [{field, default}|defaults], [type|types], true)
|
||||
end
|
||||
|
||||
defp recordp_split([other|t], defaults, types, def_type) do
|
||||
recordp_split(t, [other|defaults], [quote(do: term)|types], def_type)
|
||||
end
|
||||
|
||||
defp recordp_split([], defaults, types, def_type) do
|
||||
{:ok, :lists.reverse(defaults), :lists.reverse(types), def_type}
|
||||
end
|
||||
|
||||
defp recordp_split(_, _, _, _) do
|
||||
:error
|
||||
end
|
||||
|
||||
def deffunctions(values, env) do
|
||||
values = for value <- values, do: convert_value(value)
|
||||
escaped = Macro.escape(values)
|
||||
|
||||
contents = [
|
||||
reflection(escaped),
|
||||
initializer(escaped),
|
||||
conversions(values),
|
||||
record_optimizable(),
|
||||
updater(values),
|
||||
accessors(values, 1),
|
||||
switch_recorder()
|
||||
]
|
||||
|
||||
contents = [quote(do: @record_fields unquote(escaped))|contents]
|
||||
|
||||
# Special case for bootstrapping purposes
|
||||
if env == Macro.Env do
|
||||
Module.eval_quoted(env, contents, [], [])
|
||||
else
|
||||
Module.eval_quoted(env.module, contents, [], Macro.Env.location(env))
|
||||
end
|
||||
end
|
||||
|
||||
def deftypes(values, types, env) do
|
||||
types = types || []
|
||||
values = for value <- values do
|
||||
{name, default} = convert_value(value)
|
||||
{name, default, find_spec(types, name)}
|
||||
end
|
||||
|
||||
contents = [
|
||||
core_specs(values),
|
||||
accessor_specs(values, 1, [])
|
||||
]
|
||||
|
||||
# We need to handle bootstraping
|
||||
cond do
|
||||
:code.ensure_loaded(Kernel.Typespec) != {:module, Kernel.Typespec} ->
|
||||
nil
|
||||
env == Macro.Env ->
|
||||
Module.eval_quoted(env, contents, [], [])
|
||||
true ->
|
||||
Module.eval_quoted(env.module, contents, [], Macro.Env.location(env))
|
||||
end
|
||||
end
|
||||
|
||||
def defmacros(name, values, env, tag \\ nil)
|
||||
when is_atom(name) and is_list(values) and is_atom(tag) do
|
||||
|
||||
escaped = for value <- values do
|
||||
{key, value} = convert_value(value)
|
||||
{key, Macro.escape(value)}
|
||||
end
|
||||
|
||||
tag = tag || name
|
||||
|
||||
contents = quote do
|
||||
defmacrop unquote(name)() do
|
||||
Record.Deprecated.access(unquote(tag), unquote(escaped), [], __CALLER__)
|
||||
end
|
||||
|
||||
defmacrop unquote(name)(args) do
|
||||
Record.Deprecated.access(unquote(tag), unquote(escaped), args, __CALLER__)
|
||||
end
|
||||
|
||||
defmacrop unquote(name)(record, args) do
|
||||
Record.Deprecated.dispatch(unquote(tag), unquote(escaped), record, args, __CALLER__)
|
||||
end
|
||||
end
|
||||
|
||||
Module.eval_quoted(env.module, contents, [], Macro.Env.location(env))
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
defmacro __before_compile__(_) do
|
||||
quote do
|
||||
def __record__(:optimizable), do: @record_optimizable
|
||||
end
|
||||
end
|
||||
|
||||
def __on_definition__(env, kind, name, args, _guards, _body) do
|
||||
tuple = {name, length(args)}
|
||||
module = env.module
|
||||
functions = Module.get_attribute(module, :record_optimizable)
|
||||
|
||||
functions =
|
||||
if kind in [:def] and Module.get_attribute(module, :record_optimized) do
|
||||
[tuple|functions]
|
||||
else
|
||||
List.delete(functions, tuple)
|
||||
end
|
||||
|
||||
Module.put_attribute(module, :record_optimizable, functions)
|
||||
end
|
||||
|
||||
def access(atom, fields, arg, _caller) when is_atom(arg) do
|
||||
if index = find_index(fields, arg, 0) do
|
||||
index + 1
|
||||
else
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect arg}"
|
||||
end
|
||||
end
|
||||
|
||||
def access(atom, fields, keyword, caller) do
|
||||
unless is_keyword(keyword) do
|
||||
raise ArgumentError, message: "expected contents inside brackets to be a keyword list or an atom, got: #{inspect keyword}"
|
||||
end
|
||||
|
||||
in_match = Macro.Env.in_match?(caller)
|
||||
|
||||
has_underscore_value = Keyword.has_key?(keyword, :_)
|
||||
underscore_value = Keyword.get(keyword, :_, {:_, [], nil})
|
||||
keyword = Keyword.delete keyword, :_
|
||||
|
||||
iterator = fn({field, default}, each_keyword) ->
|
||||
new_fields =
|
||||
case Keyword.has_key?(each_keyword, field) do
|
||||
true -> Keyword.get(each_keyword, field)
|
||||
false ->
|
||||
case in_match or has_underscore_value do
|
||||
true -> underscore_value
|
||||
false -> Macro.escape(default)
|
||||
end
|
||||
end
|
||||
|
||||
{new_fields, Keyword.delete(each_keyword, field)}
|
||||
end
|
||||
|
||||
{match, remaining} = :lists.mapfoldl(iterator, keyword, fields)
|
||||
|
||||
case remaining do
|
||||
[] ->
|
||||
{:{}, [], [atom|match]}
|
||||
_ ->
|
||||
keys = for {key, _} <- remaining, do: key
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect hd(keys)}"
|
||||
end
|
||||
end
|
||||
|
||||
def dispatch(atom, fields, record, args, caller) do
|
||||
cond do
|
||||
is_atom(args) ->
|
||||
get(atom, fields, record, args)
|
||||
is_keyword(args) ->
|
||||
update(atom, fields, record, args, caller)
|
||||
true ->
|
||||
raise ArgumentError, message: "expected arguments to be a compile time atom or keywords"
|
||||
end
|
||||
end
|
||||
|
||||
defp update(atom, fields, var, keyword, caller) do
|
||||
unless is_keyword(keyword) do
|
||||
raise ArgumentError, message: "expected arguments to be compile time keywords"
|
||||
end
|
||||
|
||||
if Macro.Env.in_match?(caller) do
|
||||
raise ArgumentError, message: "cannot invoke update style macro inside match context"
|
||||
end
|
||||
|
||||
Enum.reduce keyword, var, fn({key, value}, acc) ->
|
||||
index = find_index(fields, key, 0)
|
||||
if index do
|
||||
quote do
|
||||
:erlang.setelement(unquote(index + 2), unquote(acc), unquote(value))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get(atom, fields, var, key) do
|
||||
index = find_index(fields, key, 0)
|
||||
if index do
|
||||
quote do
|
||||
:erlang.element(unquote(index + 2), unquote(var))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, message: "record #{inspect atom} does not have the key: #{inspect key}"
|
||||
end
|
||||
end
|
||||
|
||||
## Function generation
|
||||
|
||||
defp reflection(values) do
|
||||
quoted = for {k, _} <- values do
|
||||
index = find_index(values, k, 0)
|
||||
quote do
|
||||
def __record__(:index, unquote(k)), do: unquote(index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
unquote(quoted)
|
||||
|
||||
@doc false
|
||||
def __record__(:index, _), do: nil
|
||||
@doc false
|
||||
def __record__(:index, arg, _), do: __record__(:index, arg)
|
||||
|
||||
@doc false
|
||||
def __record__(kind, _), do: __record__(kind)
|
||||
|
||||
@doc false
|
||||
def __record__(:name), do: __MODULE__
|
||||
def __record__(:fields), do: unquote(values)
|
||||
end
|
||||
end
|
||||
|
||||
defp initializer(values) do
|
||||
defaults = for {_, value} <- values, do: value
|
||||
|
||||
# For each value, define a piece of code that will receive
|
||||
# an ordered dict of options (opts) and it will try to fetch
|
||||
# the given key from the ordered dict, falling back to the
|
||||
# default value if one does not exist.
|
||||
atom_selective = for {k, v} <- values, do: initialize_lookup(k, v)
|
||||
string_selective = for {k, v} <- values, do: initialize_lookup(atom_to_binary(k), v)
|
||||
|
||||
quote do
|
||||
@doc false
|
||||
def new(), do: new([])
|
||||
|
||||
@doc false
|
||||
def new([]), do: {__MODULE__, unquote_splicing(defaults)}
|
||||
def new([{key, _}|_] = opts) when is_atom(key), do: {__MODULE__, unquote_splicing(atom_selective)}
|
||||
def new([{key, _}|_] = opts) when is_binary(key), do: {__MODULE__, unquote_splicing(string_selective)}
|
||||
end
|
||||
end
|
||||
|
||||
defp initialize_lookup(k, v) do
|
||||
quote do
|
||||
case :lists.keyfind(unquote(k), 1, opts) do
|
||||
false -> unquote(v)
|
||||
{_, v} -> v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp conversions(values) do
|
||||
sorted = for {k, _} <- values do
|
||||
index = find_index(values, k, 0)
|
||||
{k, quote(do: :erlang.element(unquote(index + 2), record))}
|
||||
end
|
||||
|
||||
quote do
|
||||
@doc false
|
||||
def to_keywords(record) do
|
||||
unquote(sorted)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp accessors([{:__exception__, _}|t], 1) do
|
||||
accessors(t, 2)
|
||||
end
|
||||
|
||||
defp accessors([{key, _default}|t], i) do
|
||||
update = binary_to_atom "update_" <> atom_to_binary(key)
|
||||
|
||||
contents = quote do
|
||||
@doc false
|
||||
def unquote(key)(record) do
|
||||
:erlang.element(unquote(i + 1), record)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unquote(key)(value, record) do
|
||||
:erlang.setelement(unquote(i + 1), record, value)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unquote(update)(function, record) do
|
||||
:erlang.setelement(unquote(i + 1), record,
|
||||
function.(:erlang.element(unquote(i + 1), record)))
|
||||
end
|
||||
end
|
||||
|
||||
[contents|accessors(t, i + 1)]
|
||||
end
|
||||
|
||||
defp accessors([], _i) do
|
||||
[]
|
||||
end
|
||||
|
||||
# Define an updater method that receives a
|
||||
# keyword list and updates the record.
|
||||
defp updater(values) do
|
||||
atom_fields =
|
||||
for {key, _default} <- values, do: updater_lookup(key, key, values)
|
||||
|
||||
string_fields =
|
||||
for {key, _default} <- values, do: updater_lookup(atom_to_binary(key), key, values)
|
||||
|
||||
atom_contents = quote do: {__MODULE__, unquote_splicing(atom_fields)}
|
||||
string_contents = quote do: {__MODULE__, unquote_splicing(string_fields)}
|
||||
|
||||
quote do
|
||||
@doc false
|
||||
def update([], record) do
|
||||
record
|
||||
end
|
||||
|
||||
def update([{key, _}|_] = keywords, record) when is_atom(key) do
|
||||
unquote(atom_contents)
|
||||
end
|
||||
def update([{key, _}|_] = keywords, record) when is_binary(key) do
|
||||
unquote(string_contents)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp updater_lookup(k, key, values) do
|
||||
index = find_index(values, key, 0)
|
||||
|
||||
quote do
|
||||
case :lists.keyfind(unquote(k), 1, keywords) do
|
||||
false -> :erlang.element(unquote(index + 2), record)
|
||||
{_, value} -> value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp record_optimizable do
|
||||
quote do
|
||||
@record_optimized true
|
||||
@record_optimizable []
|
||||
@before_compile {unquote(__MODULE__), :__before_compile__}
|
||||
@on_definition {unquote(__MODULE__), :__on_definition__}
|
||||
end
|
||||
end
|
||||
|
||||
defp switch_recorder do
|
||||
quote do: @record_optimized false
|
||||
end
|
||||
|
||||
## Types/specs generation
|
||||
|
||||
defp core_specs(values) do
|
||||
types = for {_, _, spec} <- values, do: spec
|
||||
options = for {k, _, v} <- values, do: {k, v}
|
||||
|
||||
quote do
|
||||
unless Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
|
||||
@type t :: {__MODULE__, unquote_splicing(types)}
|
||||
end
|
||||
|
||||
unless Kernel.Typespec.defines_type?(__MODULE__, :options, 0) do
|
||||
@type options :: unquote(options) | [{String.t, term}]
|
||||
end
|
||||
|
||||
@spec new :: t
|
||||
@spec new(options) :: t
|
||||
@spec to_keywords(t) :: options
|
||||
@spec update(options, t) :: t
|
||||
@spec __record__(:name) :: atom
|
||||
@spec __record__(:fields) :: [{atom, any}]
|
||||
@spec __record__(:index, atom) :: non_neg_integer | nil
|
||||
end
|
||||
end
|
||||
|
||||
defp accessor_specs([{:__exception__, _, _}|t], 1, acc) do
|
||||
accessor_specs(t, 2, acc)
|
||||
end
|
||||
|
||||
defp accessor_specs([{key, _default, spec}|t], i, acc) do
|
||||
update = binary_to_atom "update_" <> atom_to_binary(key)
|
||||
|
||||
contents = quote do
|
||||
@spec unquote(key)(t) :: unquote(spec)
|
||||
@spec unquote(key)(unquote(spec), t) :: t
|
||||
@spec unquote(update)((unquote(spec) -> unquote(spec)), t) :: t
|
||||
end
|
||||
|
||||
accessor_specs(t, i + 1, [contents | acc])
|
||||
end
|
||||
|
||||
defp accessor_specs([], _i, acc), do: acc
|
||||
|
||||
## Helpers
|
||||
|
||||
defp is_keyword(list) when is_list(list), do: :lists.all(&is_keyword_tuple/1, list)
|
||||
defp is_keyword(_), do: false
|
||||
|
||||
defp is_keyword_tuple({x, _}) when is_atom(x), do: true
|
||||
defp is_keyword_tuple(_), do: false
|
||||
|
||||
defp convert_value(atom) when is_atom(atom), do: {atom, nil}
|
||||
|
||||
defp convert_value({atom, other}) when is_atom(atom), do:
|
||||
{atom, check_value(atom, other)}
|
||||
|
||||
defp convert_value({field, _}), do:
|
||||
raise(ArgumentError, message: "record field name has to be an atom, got #{inspect field}")
|
||||
|
||||
defp check_value(atom, other) when is_list(other) do
|
||||
for(i <- other, do: check_value(atom, i))
|
||||
other
|
||||
end
|
||||
|
||||
defp check_value(atom, other) when is_tuple(other) do
|
||||
for(i <- tuple_to_list(other), do: check_value(atom, i))
|
||||
other
|
||||
end
|
||||
|
||||
defp check_value(atom, other) when is_function(other) do
|
||||
unless :erlang.fun_info(other, :env) == {:env, []} and
|
||||
:erlang.fun_info(other, :type) == {:type, :external} do
|
||||
raise ArgumentError, message: "record field default value #{inspect atom} can only contain " <>
|
||||
"functions that point to an existing &Mod.fun/arity"
|
||||
end
|
||||
other
|
||||
end
|
||||
|
||||
defp check_value(atom, other) when is_reference(other) or is_pid(other) or is_port(other) do
|
||||
raise(ArgumentError, message: "record field default value #{inspect atom} cannot contain a reference, pid or port")
|
||||
end
|
||||
|
||||
defp check_value(_atom, other), do: other
|
||||
|
||||
defp find_index([{k, _}|_], k, i), do: i
|
||||
defp find_index([{_, _}|t], k, i), do: find_index(t, k, i + 1)
|
||||
defp find_index([], _k, _i), do: nil
|
||||
|
||||
defp find_spec(types, name) do
|
||||
matches = for {k, v} <- types, name == k, do: v
|
||||
case matches do
|
||||
[h|_] -> h
|
||||
_ -> quote do: term
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Record.DSL do
|
||||
@moduledoc false
|
||||
|
||||
defmacro record_type(opts) when is_list(opts) do
|
||||
escaped = for {k, v} <- opts, do: {k, Macro.escape(v)}
|
||||
|
||||
quote do
|
||||
@record_types Keyword.merge(@record_types || [], unquote(escaped))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,92 +0,0 @@
|
||||
defmodule Record.Extractor do
|
||||
@moduledoc false
|
||||
|
||||
# Retrieve a record definition from an Erlang file using
|
||||
# the same lookup as the *include* attribute from Erlang modules.
|
||||
def extract(name, from: file) when is_binary(file) do
|
||||
file = List.from_char_data!(file)
|
||||
|
||||
realfile =
|
||||
case :code.where_is_file(file) do
|
||||
:non_existing -> file
|
||||
realfile -> realfile
|
||||
end
|
||||
|
||||
extract_record(name, realfile)
|
||||
end
|
||||
|
||||
# Retrieve a record definition from an Erlang file using
|
||||
# the same lookup as the *include_lib* attribute from Erlang modules.
|
||||
def extract(name, from_lib: file) when is_binary(file) do
|
||||
[app|path] = :filename.split(List.from_char_data!(file))
|
||||
|
||||
case :code.lib_dir(list_to_atom(app)) do
|
||||
{:error, _} ->
|
||||
raise ArgumentError, message: "lib file #{file} could not be found"
|
||||
libpath ->
|
||||
extract_record name, :filename.join([libpath|path])
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieve the record with the given name from the given file
|
||||
defp extract_record(name, file) do
|
||||
form = read_file(file)
|
||||
records = extract_records(form)
|
||||
if record = List.keyfind(records, name, 0) do
|
||||
parse_record(record, form)
|
||||
else
|
||||
raise ArgumentError, message: "no record #{name} found at #{file}"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the given file and extract all existent records.
|
||||
defp extract_records(form) do
|
||||
for {:attribute, _, :record, record} <- form, do: record
|
||||
end
|
||||
|
||||
# Read a file and return its abstract syntax form that also
|
||||
# includes record and other preprocessor modules. This is done
|
||||
# by using Erlang's epp_dodger.
|
||||
defp read_file(file) do
|
||||
case :epp_dodger.quick_parse_file(file) do
|
||||
{:ok, form} ->
|
||||
form
|
||||
other ->
|
||||
raise "error parsing file #{file}, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a tuple with name and fields and returns a
|
||||
# list of tuples where the first element is the field
|
||||
# and the second is its default value.
|
||||
defp parse_record({_name, fields}, form) do
|
||||
cons = List.foldr fields, {nil, 0}, fn f, acc ->
|
||||
{:cons, 0, parse_field(f), acc}
|
||||
end
|
||||
eval_record(cons, form)
|
||||
end
|
||||
|
||||
defp parse_field({:typed_record_field, record_field, _type}) do
|
||||
parse_field(record_field)
|
||||
end
|
||||
|
||||
defp parse_field({:record_field, _, key}) do
|
||||
{:tuple, 0, [key, {:atom, 0, :undefined}]}
|
||||
end
|
||||
|
||||
defp parse_field({:record_field, _, key, value}) do
|
||||
{:tuple, 0, [key, value]}
|
||||
end
|
||||
|
||||
defp eval_record(cons, form) do
|
||||
form = form ++
|
||||
[ {:function, 0, :hello, 0, [
|
||||
{:clause, 0, [], [], [ cons ]} ]} ]
|
||||
|
||||
{:function, 0, :hello, 0, [
|
||||
{:clause, 0, [], [], [ record_ast ]} ]} = :erl_expand_records.module(form, []) |> List.last
|
||||
|
||||
{:value, record, _} = :erl_eval.expr(record_ast, [])
|
||||
record
|
||||
end
|
||||
end
|
||||
@@ -1,574 +0,0 @@
|
||||
defmodule Regex do
|
||||
@moduledoc ~S"""
|
||||
Regular expressions for Elixir built on top of Erlang's `re` module.
|
||||
|
||||
As the `re` module, Regex is based on PCRE
|
||||
(Perl Compatible Regular Expressions). More information can be
|
||||
found in the [`re` documentation](http://www.erlang.org/doc/man/re.html).
|
||||
|
||||
Regular expressions in Elixir can be created using `Regex.compile!/2`
|
||||
or using the special form with [`~r`](Kernel.html#sigil_r/2):
|
||||
|
||||
# A simple regular expressions that matches foo anywhere in the string
|
||||
~r/foo/
|
||||
|
||||
# A regular expression with case insensitive and unicode options
|
||||
~r/foo/iu
|
||||
|
||||
A Regex is represented internally as the `Regex` struct. Therefore,
|
||||
`%Regex{}` can be used whenever there is a need to match on them.
|
||||
|
||||
## Modifiers
|
||||
|
||||
The modifiers available when creating a Regex are:
|
||||
|
||||
* `unicode` (u) - enables unicode specific patterns like \p. it expects valid unicode
|
||||
strings to be given on match
|
||||
* `caseless` (i) - add case insensitivity
|
||||
* `dotall` (s) - causes dot to match newlines and also set newline to anycrlf.
|
||||
The new line setting can be overridden by setting `(*CR)` or `(*LF)` or
|
||||
`(*CRLF)` or `(*ANY)` according to re documentation
|
||||
* `multiline` (m) - causes `^` and `$` to mark the beginning and end of each line.
|
||||
Use `\A` and `\z` to match the end or beginning of the string
|
||||
* `extended` (x) - whitespace characters are ignored except when escaped and
|
||||
allow `#` to delimit comments
|
||||
* `firstline` (f) - forces the unanchored pattern to match before or at the first
|
||||
newline, though the matched text may continue over the newline
|
||||
* `ungreedy` (r) - inverts the "greediness" of the regexp
|
||||
|
||||
The options not available are:
|
||||
|
||||
* `anchored` - not available, use `^` or `\A` instead
|
||||
* `dollar_endonly` - not available, use `\z` instead
|
||||
* `no_auto_capture` - not available, use `?:` instead
|
||||
* `newline` - not available, use `(*CR)` or `(*LF)` or `(*CRLF)` or `(*ANYCRLF)`
|
||||
or `(*ANY)` at the beginning of the regexp according to the re documentation
|
||||
|
||||
## Captures
|
||||
|
||||
Many functions in this module allows what to capture in a regex
|
||||
match via the `:capture` option. The supported values are:
|
||||
|
||||
* `:all` - all captured subpatterns including the complete matching string.
|
||||
This is the default;
|
||||
|
||||
* `:first` - only the first captured subpattern, which is always the complete
|
||||
matching part of the string. All explicitly captured subpatterns are
|
||||
discarded;
|
||||
|
||||
* `:all_but_first`- all but the first matching subpattern, i.e. all explicitly
|
||||
captured subpatterns, but not the complete matching part of
|
||||
the string;
|
||||
|
||||
* `:none` - do not return matching subpatterns at all;
|
||||
|
||||
* `:all_names` - captures all names in the Regex;
|
||||
|
||||
* `list(binary)` - a list of named captures to capture;
|
||||
|
||||
"""
|
||||
|
||||
defstruct re_pattern: nil :: term, source: "" :: binary, opts: "" :: binary
|
||||
|
||||
defexception CompileError, message: "regex could not be compiled"
|
||||
|
||||
@doc """
|
||||
Compiles the regular expression.
|
||||
|
||||
The given options can either be a binary with the characters
|
||||
representing the same regex options given to the `~r` sigil,
|
||||
or a list of options, as expected by the [Erlang `re` docs](http://www.erlang.org/doc/man/re.html).
|
||||
|
||||
It returns `{:ok, regex}` in case of success,
|
||||
`{:error, reason}` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.compile("foo")
|
||||
{:ok, ~r"foo"}
|
||||
|
||||
iex> Regex.compile("*foo")
|
||||
{:error, {'nothing to repeat', 0}}
|
||||
|
||||
"""
|
||||
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
|
||||
def compile(source, options \\ "")
|
||||
|
||||
def compile(source, options) when is_binary(options) do
|
||||
case translate_options(options) do
|
||||
{:error, rest} ->
|
||||
{:error, {:invalid_option, rest}}
|
||||
|
||||
translated_options ->
|
||||
compile(source, translated_options, options)
|
||||
end
|
||||
end
|
||||
|
||||
def compile(source, options) when is_list(options) do
|
||||
compile(source, options, "")
|
||||
end
|
||||
|
||||
defp compile(source, opts, doc_opts) when is_binary(source) do
|
||||
case :re.compile(source, opts) do
|
||||
{:ok, re_pattern} ->
|
||||
{:ok, %Regex{re_pattern: re_pattern, source: source, opts: doc_opts}}
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the regular expression according to the given options.
|
||||
Fails with `Regex.CompileError` if the regex cannot be compiled.
|
||||
"""
|
||||
def compile!(source, options \\ "") do
|
||||
case compile(source, options) do
|
||||
{:ok, regex} -> regex
|
||||
{:error, {reason, at}} -> raise Regex.CompileError, message: "#{reason} at position #{at}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a boolean indicating whether there was a match or not.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.match?(~r/foo/, "foo")
|
||||
true
|
||||
|
||||
iex> Regex.match?(~r/foo/, "bar")
|
||||
false
|
||||
|
||||
"""
|
||||
def match?(%Regex{re_pattern: compiled}, string) when is_binary(string) do
|
||||
:re.run(string, compiled, [{:capture, :none}]) == :match
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument is a regex.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.regex?(~r/foo/)
|
||||
true
|
||||
|
||||
iex> Regex.regex?(0)
|
||||
false
|
||||
|
||||
"""
|
||||
def regex?(%Regex{}), do: true
|
||||
def regex?(_), do: false
|
||||
|
||||
@doc """
|
||||
Runs the regular expression against the given string until the first match.
|
||||
It returns a list with all captures or `nil` if no match occurred.
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - Set to `:index` to return indexes. Defaults to `:binary`;
|
||||
* `:capture` - What to capture in the result. Check the moduledoc for Regex
|
||||
to see the possible capture values;
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.run(~r/c(d)/, "abcd")
|
||||
["cd", "d"]
|
||||
|
||||
iex> Regex.run(~r/e/, "abcd")
|
||||
nil
|
||||
|
||||
iex> Regex.run(~r/c(d)/, "abcd", return: :index)
|
||||
[{2,2},{3,1}]
|
||||
|
||||
"""
|
||||
def run(regex, string, options \\ [])
|
||||
|
||||
def run(%Regex{re_pattern: compiled}, string, options) when is_binary(string) do
|
||||
return = Keyword.get(options, :return, :binary)
|
||||
captures = Keyword.get(options, :capture, :all)
|
||||
|
||||
case :re.run(string, compiled, [{:capture, captures, return}]) do
|
||||
:nomatch -> nil
|
||||
:match -> []
|
||||
{:match, results} -> results
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the given captures as a map or `nil` if no captures are
|
||||
found. The option `:return` can be set to `:index` to get indexes
|
||||
back.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.named_captures(~r/c(?<foo>d)/, "abcd")
|
||||
%{"foo" => "d"}
|
||||
|
||||
iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)/, "abcd")
|
||||
%{"bar" => "d", "foo" => "b"}
|
||||
|
||||
iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)/, "efgh")
|
||||
nil
|
||||
|
||||
"""
|
||||
def named_captures(regex, string, options \\ []) when is_binary(string) do
|
||||
names = names(regex)
|
||||
options = Keyword.put(options, :capture, names)
|
||||
results = run(regex, string, options)
|
||||
if results, do: Enum.zip(names, results) |> Enum.into(%{})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the underlying `re_pattern` in the regular expression.
|
||||
"""
|
||||
def re_pattern(%Regex{re_pattern: compiled}) do
|
||||
compiled
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the regex source as a binary.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.source(~r(foo))
|
||||
"foo"
|
||||
|
||||
"""
|
||||
def source(%Regex{source: source}) do
|
||||
source
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the regex options as a string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.opts(~r(foo)m)
|
||||
"m"
|
||||
|
||||
"""
|
||||
def opts(%Regex{opts: opts}) do
|
||||
opts
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of names in the regex.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.names(~r/(?<foo>bar)/)
|
||||
["foo"]
|
||||
|
||||
"""
|
||||
def names(%Regex{re_pattern: re_pattern}) do
|
||||
{:namelist, names} = :re.inspect(re_pattern, :namelist)
|
||||
names
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `run/3`, but scans the target several times collecting all
|
||||
matches of the regular expression. A list of lists is returned,
|
||||
where each entry in the primary list represents a match and each
|
||||
entry in the secondary list represents the captured contents.
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - Set to `:index` to return indexes. Defaults to `:binary`;
|
||||
* `:capture` - What to capture in the result. Check the moduledoc for Regex
|
||||
to see the possible capture values;
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.scan(~r/c(d|e)/, "abcd abce")
|
||||
[["cd", "d"], ["ce", "e"]]
|
||||
|
||||
iex> Regex.scan(~r/c(?:d|e)/, "abcd abce")
|
||||
[["cd"], ["ce"]]
|
||||
|
||||
iex> Regex.scan(~r/e/, "abcd")
|
||||
[]
|
||||
|
||||
"""
|
||||
def scan(regex, string, options \\ [])
|
||||
|
||||
def scan(%Regex{re_pattern: compiled}, string, options) when is_binary(string) do
|
||||
return = Keyword.get(options, :return, :binary)
|
||||
captures = Keyword.get(options, :capture, :all)
|
||||
options = [{:capture, captures, return}, :global]
|
||||
|
||||
case :re.run(string, compiled, options) do
|
||||
:match -> []
|
||||
:nomatch -> []
|
||||
{:match, results} -> results
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits the given target into the number of parts specified.
|
||||
|
||||
## Options
|
||||
|
||||
* `:parts` - when specified, splits the string into the
|
||||
given number of parts. If not specified, `:parts`
|
||||
is defaulted to `:infinity`, which will split the
|
||||
string into the maximum number of parts possible
|
||||
based on the given pattern.
|
||||
|
||||
* `:trim` - when true, remove blank strings from the result;
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.split(~r/-/, "a-b-c")
|
||||
["a","b","c"]
|
||||
|
||||
iex> Regex.split(~r/-/, "a-b-c", [parts: 2])
|
||||
["a","b-c"]
|
||||
|
||||
iex> Regex.split(~r/-/, "abc")
|
||||
["abc"]
|
||||
|
||||
iex> Regex.split(~r//, "abc")
|
||||
["a", "b", "c", ""]
|
||||
|
||||
iex> Regex.split(~r//, "abc", trim: true)
|
||||
["a", "b", "c"]
|
||||
|
||||
"""
|
||||
|
||||
def split(regex, string, options \\ [])
|
||||
|
||||
def split(%Regex{re_pattern: compiled}, string, options) when is_binary(string) do
|
||||
if options[:global] != nil do
|
||||
IO.write :stderr, "Support for :global in Regex.split/3 is deprecated, please use :parts instead\n#{Exception.format_stacktrace}"
|
||||
end
|
||||
parts =
|
||||
cond do
|
||||
Keyword.get(options, :global) == false -> 2
|
||||
p = Keyword.get(options, :parts) -> if p == 0, do: :infinity, else: p
|
||||
true -> :infinity
|
||||
end
|
||||
|
||||
opts = [return: :binary, parts: parts]
|
||||
splits = :re.split(string, compiled, opts)
|
||||
|
||||
if Keyword.get(options, :trim, false) do
|
||||
for split <- splits, split != "", do: split
|
||||
else
|
||||
splits
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Receives a regex, a binary and a replacement, returns a new
|
||||
binary where the all matches are replaced by replacement.
|
||||
|
||||
The replacement can be either a string or a function. The string
|
||||
is used as a replacement for every match and it allows specific
|
||||
captures to be accessed via `\N`, where `N` is the capture. In
|
||||
case `\0` is used, the whole match is inserted.
|
||||
|
||||
When the replacement is a function, the function may have arity
|
||||
N where each argument maps to a capture, with the first argument
|
||||
being the whole match. If the function expects more arguments
|
||||
than captures found, the remaining arguments will receive `""`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:global` - when `false`, replaces only the first occurrence
|
||||
(defaults to true)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.replace(~r/d/, "abc", "d")
|
||||
"abc"
|
||||
|
||||
iex> Regex.replace(~r/b/, "abc", "d")
|
||||
"adc"
|
||||
|
||||
iex> Regex.replace(~r/b/, "abc", "[\\0]")
|
||||
"a[b]c"
|
||||
|
||||
iex> Regex.replace(~r/a(b|d)c/, "abcadc", "[\\1]")
|
||||
"[b][d]"
|
||||
|
||||
iex> Regex.replace(~r/a(b|d)c/, "abcadc", fn _, x -> "[#{x}]" end)
|
||||
"[b][d]"
|
||||
|
||||
"""
|
||||
def replace(regex, string, replacement, options \\ [])
|
||||
|
||||
def replace(regex, string, replacement, options) when is_binary(replacement) do
|
||||
do_replace(regex, string, precompile_replacement(replacement), options)
|
||||
end
|
||||
|
||||
def replace(regex, string, replacement, options) when is_function(replacement) do
|
||||
{:arity, arity} = :erlang.fun_info(replacement, :arity)
|
||||
do_replace(regex, string, {replacement, arity}, options)
|
||||
end
|
||||
|
||||
defp do_replace(%Regex{re_pattern: compiled}, string, replacement, options) do
|
||||
opts = if Keyword.get(options, :global) != false, do: [:global], else: []
|
||||
opts = [{:capture, :all, :index}|opts]
|
||||
|
||||
case :re.run(string, compiled, opts) do
|
||||
:nomatch ->
|
||||
string
|
||||
{:match, [mlist|t]} when is_list(mlist) ->
|
||||
apply_list(string, replacement, [mlist|t]) |> iodata_to_binary
|
||||
{:match, slist} ->
|
||||
apply_list(string, replacement, [slist]) |> iodata_to_binary
|
||||
end
|
||||
end
|
||||
|
||||
defp precompile_replacement(""),
|
||||
do: []
|
||||
|
||||
defp precompile_replacement(<<?\\, x, rest :: binary>>) when x < ?0 or x > ?9 do
|
||||
case precompile_replacement(rest) do
|
||||
[head | t] when is_binary(head) ->
|
||||
[<<x, head :: binary>> | t]
|
||||
other ->
|
||||
[<<x>> | other]
|
||||
end
|
||||
end
|
||||
|
||||
defp precompile_replacement(<<?\\, rest :: binary>>) when byte_size(rest) > 0 do
|
||||
{ns, rest} = pick_int(rest)
|
||||
[list_to_integer(ns) | precompile_replacement(rest)]
|
||||
end
|
||||
|
||||
defp precompile_replacement(<<x, rest :: binary>>) do
|
||||
case precompile_replacement(rest) do
|
||||
[head | t] when is_binary(head) ->
|
||||
[<<x, head :: binary>> | t]
|
||||
other ->
|
||||
[<<x>> | other]
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_int(<<x, rest :: binary>>) when x in ?0..?9 do
|
||||
{found, rest} = pick_int(rest)
|
||||
{[x|found], rest}
|
||||
end
|
||||
|
||||
defp pick_int(bin) do
|
||||
{[], bin}
|
||||
end
|
||||
|
||||
defp apply_list(string, replacement, list) do
|
||||
apply_list(string, string, 0, replacement, list)
|
||||
end
|
||||
|
||||
defp apply_list(_, "", _, _, []) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp apply_list(_, string, _, _, []) do
|
||||
string
|
||||
end
|
||||
|
||||
defp apply_list(whole, string, pos, replacement, [[{mpos, _} | _] | _] = list) when mpos > pos do
|
||||
length = mpos - pos
|
||||
<<untouched :: [size(length), binary], rest :: binary>> = string
|
||||
[untouched | apply_list(whole, rest, mpos, replacement, list)]
|
||||
end
|
||||
|
||||
defp apply_list(whole, string, pos, replacement, [[{mpos, length} | _] = head | tail]) when mpos == pos do
|
||||
<<_ :: [size(length), binary], rest :: binary>> = string
|
||||
new_data = apply_replace(whole, replacement, head)
|
||||
[new_data | apply_list(whole, rest, pos + length, replacement, tail)]
|
||||
end
|
||||
|
||||
defp apply_replace(string, {fun, arity}, indexes) do
|
||||
apply(fun, get_indexes(string, indexes, arity))
|
||||
end
|
||||
|
||||
defp apply_replace(_, [bin], _) when is_binary(bin) do
|
||||
bin
|
||||
end
|
||||
|
||||
defp apply_replace(string, repl, indexes) do
|
||||
indexes = list_to_tuple(indexes)
|
||||
|
||||
for part <- repl do
|
||||
cond do
|
||||
is_binary(part) ->
|
||||
part
|
||||
part > tuple_size(indexes) ->
|
||||
""
|
||||
true ->
|
||||
get_index(string, elem(indexes, part))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_index(_string, {pos, _len}) when pos < 0 do
|
||||
""
|
||||
end
|
||||
|
||||
defp get_index(string, {pos, len}) do
|
||||
<<_ :: [size(pos), binary], res :: [size(len), binary], _ :: binary>> = string
|
||||
res
|
||||
end
|
||||
|
||||
defp get_indexes(_string, _, 0) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp get_indexes(string, [], arity) do
|
||||
[""|get_indexes(string, [], arity - 1)]
|
||||
end
|
||||
|
||||
defp get_indexes(string, [h|t], arity) do
|
||||
[get_index(string, h)|get_indexes(string, t, arity - 1)]
|
||||
end
|
||||
|
||||
{:ok, pattern} = :re.compile(~S"[.^$*+?()[{\\\|\s#]", [:unicode])
|
||||
@escape_pattern pattern
|
||||
|
||||
@doc ~S"""
|
||||
Escapes a string to be literally matched in a regex.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.escape(".")
|
||||
"\\."
|
||||
|
||||
iex> Regex.escape("\\what if")
|
||||
"\\\\what\\ if"
|
||||
|
||||
"""
|
||||
@spec escape(String.t) :: String.t
|
||||
def escape(string) when is_binary(string) do
|
||||
:re.replace(string, @escape_pattern, "\\\\&", [:global, {:return, :binary}])
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
@doc false
|
||||
# Unescape map function used by Macro.unescape_string.
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(_), do: false
|
||||
|
||||
# Private Helpers
|
||||
|
||||
defp translate_options(<<?g, t :: binary>>) do
|
||||
IO.write :stderr, "The /g flag for regular expressions is no longer needed\n#{Exception.format_stacktrace}"
|
||||
translate_options(t)
|
||||
end
|
||||
|
||||
defp translate_options(<<?u, t :: binary>>), do: [:unicode|translate_options(t)]
|
||||
defp translate_options(<<?i, t :: binary>>), do: [:caseless|translate_options(t)]
|
||||
defp translate_options(<<?x, t :: binary>>), do: [:extended|translate_options(t)]
|
||||
defp translate_options(<<?f, t :: binary>>), do: [:firstline|translate_options(t)]
|
||||
defp translate_options(<<?r, t :: binary>>), do: [:ungreedy|translate_options(t)]
|
||||
defp translate_options(<<?s, t :: binary>>), do: [:dotall, {:newline, :anycrlf}|translate_options(t)]
|
||||
defp translate_options(<<?m, t :: binary>>), do: [:multiline|translate_options(t)]
|
||||
defp translate_options(<<>>), do: []
|
||||
defp translate_options(rest), do: {:error, rest}
|
||||
end
|
||||
@@ -1,339 +0,0 @@
|
||||
defmodule Set do
|
||||
@moduledoc ~S"""
|
||||
This module specifies the Set API expected to be
|
||||
implemented by different representations.
|
||||
|
||||
It also provides functions that redirect to the
|
||||
underlying Set, allowing a developer to work with
|
||||
different Set implementations using one API.
|
||||
|
||||
To create a new set, use the `new` functions defined
|
||||
by each set type:
|
||||
|
||||
HashSet.new #=> creates an empty HashSet
|
||||
|
||||
In the examples below, `set_impl` means a specific
|
||||
`Set` implementation, for example `HashSet`.
|
||||
|
||||
## Protocols
|
||||
|
||||
Sets are required to implement both `Enumerable` and `Collectable`
|
||||
protocols.
|
||||
|
||||
## Match
|
||||
|
||||
Sets are required to implement all operations using the match (`===`)
|
||||
operator.
|
||||
"""
|
||||
|
||||
use Behaviour
|
||||
|
||||
@type value :: any
|
||||
@type values :: [ value ]
|
||||
@type t :: map
|
||||
|
||||
defcallback new :: t
|
||||
defcallback delete(t, value) :: t
|
||||
defcallback difference(t, t) :: t
|
||||
defcallback disjoint?(t, t) :: boolean
|
||||
defcallback equal?(t, t) :: boolean
|
||||
defcallback intersection(t, t) :: t
|
||||
defcallback member?(t, value) :: boolean
|
||||
defcallback put(t, value) :: t
|
||||
defcallback size(t) :: non_neg_integer
|
||||
defcallback subset?(t, t) :: boolean
|
||||
defcallback to_list(t) :: list()
|
||||
defcallback union(t, t) :: t
|
||||
|
||||
defmacrop target(set) do
|
||||
quote do
|
||||
case unquote(set) do
|
||||
%{__struct__: x} when is_atom(x) ->
|
||||
x
|
||||
x when is_tuple(x) ->
|
||||
IO.write :stderr, "Using records with the Set module is deprecated, please use structs instead\n#{Exception.format_stacktrace}"
|
||||
elem(x, 0)
|
||||
x ->
|
||||
unsupported_set(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes `value` from `set`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> s = Enum.into([1, 2, 3], set_impl.new)
|
||||
iex> Set.delete(s, 4) |> Enum.sort
|
||||
[1, 2, 3]
|
||||
|
||||
iex> s = Enum.into([1, 2, 3], set_impl.new)
|
||||
iex> Set.delete(s, 2) |> Enum.sort
|
||||
[1, 3]
|
||||
|
||||
"""
|
||||
@spec delete(t, value) :: t
|
||||
def delete(set, value) do
|
||||
target(set).delete(set, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a set that is `set1` without the members of `set2`.
|
||||
|
||||
Notice this function is polymorphic as it calculates the difference
|
||||
for of any type. Each set implementation also provides a `difference`
|
||||
function, but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.difference(Enum.into([1,2], set_impl.new), Enum.into([2,3,4], set_impl.new)) |> Enum.sort
|
||||
[1]
|
||||
|
||||
"""
|
||||
@spec difference(t, t) :: t
|
||||
def difference(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.difference(set1, set2)
|
||||
else
|
||||
target2.reduce(set2, {:cont, set1}, fn v, acc ->
|
||||
{:cont, target1.delete(acc, v)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if `set1` and `set2` have no members in common.
|
||||
|
||||
Notice this function is polymorphic as it checks for disjoint sets of
|
||||
any type. Each set implementation also provides a `disjoint?` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.disjoint?(Enum.into([1, 2], set_impl.new), Enum.into([3, 4], set_impl.new))
|
||||
true
|
||||
|
||||
iex> Set.disjoint?(Enum.into([1, 2], set_impl.new), Enum.into([2, 3], set_impl.new))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec disjoint?(t, t) :: boolean
|
||||
def disjoint?(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.disjoint?(set1, set2)
|
||||
else
|
||||
target2.reduce(set2, {:cont, true}, fn member, acc ->
|
||||
case target1.member?(set1, member) do
|
||||
false -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec empty(t) :: t
|
||||
def empty(set) do
|
||||
target(set).empty(set)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if two sets are equal using `===`.
|
||||
|
||||
Notice this function is polymorphic as it compares sets of
|
||||
any type. Each set implementation also provides an `equal?`
|
||||
function, but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.equal?(Enum.into([1, 2], set_impl.new), Enum.into([2, 1, 1], set_impl.new))
|
||||
true
|
||||
|
||||
iex> Set.equal?(Enum.into([1, 2], set_impl.new), Enum.into([3, 4], set_impl.new))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
cond do
|
||||
target1 == target2 ->
|
||||
target1.equal?(set1, set2)
|
||||
|
||||
target1.size(set1) == target2.size(set2) ->
|
||||
do_subset?(target1, target2, set1, set2)
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a set containing only members in common between `set1` and `set2`.
|
||||
|
||||
Notice this function is polymorphic as it calculates the intersection of
|
||||
any type. Each set implementation also provides a `intersection` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.intersection(Enum.into([1,2], set_impl.new), Enum.into([2,3,4], set_impl.new)) |> Enum.sort
|
||||
[2]
|
||||
|
||||
iex> Set.intersection(Enum.into([1,2], set_impl.new), Enum.into([3,4], set_impl.new)) |> Enum.sort
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec intersection(t, t) :: t
|
||||
def intersection(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.intersection(set1, set2)
|
||||
else
|
||||
target1.reduce(set1, {:cont, Collectable.empty(set1)}, fn v, acc ->
|
||||
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if `set` contains `value`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.member?(Enum.into([1, 2, 3], set_impl.new), 2)
|
||||
true
|
||||
|
||||
iex> Set.member?(Enum.into([1, 2, 3], set_impl.new), 4)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec member?(t, value) :: boolean
|
||||
def member?(set, value) do
|
||||
target(set).member?(set, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inserts `value` into `set` if it does not already contain it.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.put(Enum.into([1, 2, 3], set_impl.new), 3) |> Enum.sort
|
||||
[1, 2, 3]
|
||||
|
||||
iex> Set.put(Enum.into([1, 2, 3], set_impl.new), 4) |> Enum.sort
|
||||
[1, 2, 3, 4]
|
||||
|
||||
"""
|
||||
@spec put(t, value) :: t
|
||||
def put(set, value) do
|
||||
target(set).put(set, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of elements in `set`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.size(Enum.into([1, 2, 3], set_impl.new))
|
||||
3
|
||||
|
||||
"""
|
||||
@spec size(t) :: non_neg_integer
|
||||
def size(set) do
|
||||
target(set).size(set)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if `set1`'s members are all contained in `set2`.
|
||||
|
||||
Notice this function is polymorphic as it checks the subset for
|
||||
any type. Each set implementation also provides a `subset?` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.subset?(Enum.into([1, 2], set_impl.new), Enum.into([1, 2, 3], set_impl.new))
|
||||
true
|
||||
|
||||
iex> Set.subset?(Enum.into([1, 2, 3], set_impl.new), Enum.into([1, 2], set_impl.new))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec subset?(t, t) :: boolean
|
||||
def subset?(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.subset?(set1, set2)
|
||||
else
|
||||
do_subset?(target1, target2, set1, set2)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts `set` to a list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> set_impl.to_list(Enum.into([1, 2, 3], set_impl.new)) |> Enum.sort
|
||||
[1,2,3]
|
||||
|
||||
"""
|
||||
@spec to_list(t) :: list
|
||||
def to_list(set) do
|
||||
target(set).to_list(set)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a set containing all members of `set1` and `set2`.
|
||||
|
||||
Notice this function is polymorphic as it calculates the union of
|
||||
any type. Each set implementation also provides a `union` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.union(Enum.into([1,2], set_impl.new), Enum.into([2,3,4], set_impl.new)) |> Enum.sort
|
||||
[1,2,3,4]
|
||||
|
||||
"""
|
||||
@spec union(t, t) :: t
|
||||
def union(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.union(set1, set2)
|
||||
else
|
||||
target2.reduce(set2, {:cont, set1}, fn v, acc ->
|
||||
{:cont, target1.put(acc, v)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_subset?(target1, target2, set1, set2) do
|
||||
target1.reduce(set1, {:cont, true}, fn member, acc ->
|
||||
case target2.member?(set2, member) do
|
||||
true -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
defp unsupported_set(set) do
|
||||
raise ArgumentError, message: "unsupported set: #{inspect set}"
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
defmodule Stream.Reducers do
|
||||
# Collection of reducers shared by Enum and Stream.
|
||||
@moduledoc false
|
||||
|
||||
defmacro chunk(n, step, limit, f \\ nil) do
|
||||
quote do
|
||||
fn entry, acc(h, {buffer, count}, t) ->
|
||||
buffer = [entry|buffer]
|
||||
count = count + 1
|
||||
|
||||
new =
|
||||
if count >= unquote(limit) do
|
||||
left = count - unquote(step)
|
||||
{Enum.take(buffer, left), left}
|
||||
else
|
||||
{buffer, count}
|
||||
end
|
||||
|
||||
if count == unquote(n) do
|
||||
cont_with_acc(unquote(f), :lists.reverse(buffer), h, new, t)
|
||||
else
|
||||
{:cont, acc(h, new, t)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro chunk_by(callback, f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
entry, acc(h, {buffer, value}, t) ->
|
||||
new_value = unquote(callback).(entry)
|
||||
if new_value == value do
|
||||
{:cont, acc(h, {[entry|buffer], value}, t)}
|
||||
else
|
||||
cont_with_acc(unquote(f), :lists.reverse(buffer), h, {[entry], new_value}, t)
|
||||
end
|
||||
entry, acc(h, nil, t) ->
|
||||
{:cont, acc(h, {[entry], unquote(callback).(entry)}, t)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro drop(f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
_entry, acc(h, n, t) when n > 0 ->
|
||||
{:cont, acc(h, n-1, t)}
|
||||
entry, acc(h, n, t) ->
|
||||
cont_with_acc(unquote(f), entry, h, n, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro drop_while(callback, f \\ nil) do
|
||||
quote do
|
||||
fn entry, acc(h, bool, t) = orig ->
|
||||
if bool and unquote(callback).(entry) do
|
||||
{:cont, orig}
|
||||
else
|
||||
cont_with_acc(unquote(f), entry, h, false, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro filter(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
if unquote(callback).(entry) do
|
||||
cont(unquote(f), entry, acc)
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro filter_map(filter, mapper, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
if unquote(filter).(entry) do
|
||||
cont(unquote(f), unquote(mapper).(entry), acc)
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro map(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
cont(unquote(f), unquote(callback).(entry), acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro reject(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
unless unquote(callback).(entry) do
|
||||
cont(unquote(f), entry, acc)
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro scan_2(callback, f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
entry, acc(h, :first, t) ->
|
||||
cont_with_acc(unquote(f), entry, h, {:ok, entry}, t)
|
||||
entry, acc(h, {:ok, acc}, t) ->
|
||||
value = unquote(callback).(entry, acc)
|
||||
cont_with_acc(unquote(f), value, h, {:ok, value}, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro scan_3(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, acc, t)) ->
|
||||
value = unquote(callback).(entry, acc)
|
||||
cont_with_acc(unquote(f), value, h, value, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro take(f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, n, t) = orig) ->
|
||||
if n >= 1 do
|
||||
cont_with_acc(unquote(f), entry, h, n-1, t)
|
||||
else
|
||||
{:halt, orig}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro take_every(nth, f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
entry, acc(h, n, t) when n === :first
|
||||
when n === unquote(nth) ->
|
||||
cont_with_acc(unquote(f), entry, h, 1, t)
|
||||
entry, acc(h, n, t) ->
|
||||
{:cont, acc(h, n+1, t)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro take_while(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
if unquote(callback).(entry) do
|
||||
cont(unquote(f), entry, acc)
|
||||
else
|
||||
{:halt, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro uniq(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, prev, t) = acc) ->
|
||||
value = unquote(callback).(entry)
|
||||
if :lists.member(value, prev) do
|
||||
{:cont, acc}
|
||||
else
|
||||
cont_with_acc(unquote(f), entry, h, [value|prev], t)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro with_index(f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, counter, t)) ->
|
||||
cont_with_acc(unquote(f), {entry, counter}, h, counter + 1, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,85 +0,0 @@
|
||||
import Kernel, except: [to_string: 1]
|
||||
|
||||
defprotocol String.Chars do
|
||||
@moduledoc ~S"""
|
||||
The String.Chars protocol is responsible for
|
||||
converting a structure to a Binary (only if applicable).
|
||||
The only function required to be implemented is
|
||||
`to_string` which does the conversion.
|
||||
|
||||
The `to_string` function automatically imported
|
||||
by Kernel invokes this protocol. String
|
||||
interpolation also invokes to_string in its
|
||||
arguments. For example, `"foo#{bar}"` is the same
|
||||
as `"foo" <> to_string(bar)`.
|
||||
"""
|
||||
|
||||
def to_string(thing)
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Atom do
|
||||
@doc """
|
||||
Convert the atom literally to a binary, except
|
||||
`nil` which is converted to an empty string.
|
||||
"""
|
||||
def to_string(nil) do
|
||||
""
|
||||
end
|
||||
|
||||
def to_string(atom) do
|
||||
atom_to_binary(atom)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: BitString do
|
||||
@doc """
|
||||
Returns the given binary or raises an error for bitstrings.
|
||||
"""
|
||||
def to_string(thing) when is_binary(thing) do
|
||||
thing
|
||||
end
|
||||
|
||||
def to_string(thing) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: thing,
|
||||
description: "cannot convert a bitstring to a string"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: List do
|
||||
@doc """
|
||||
Consider the list is an iolist and converts it
|
||||
to a binary. This allows a list of binaries, or
|
||||
a charlist, or a mix of both, to be converted
|
||||
successfully.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> to_string('foo')
|
||||
"foo"
|
||||
|
||||
iex> to_string(["foo", 'bar'])
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
def to_string(char_list), do: String.from_char_data!(char_list)
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Integer do
|
||||
@doc """
|
||||
Simply converts the integer to a string.
|
||||
"""
|
||||
def to_string(thing) do
|
||||
integer_to_binary(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Float do
|
||||
@doc """
|
||||
Simply converts the float to a string.
|
||||
"""
|
||||
def to_string(thing) do
|
||||
iodata_to_binary(:io_lib_format.fwrite_g(thing))
|
||||
end
|
||||
end
|
||||
@@ -1,340 +0,0 @@
|
||||
defmodule StringIO do
|
||||
@moduledoc """
|
||||
This module provides an IO device that wraps a string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("foo")
|
||||
iex> IO.read(pid, 2)
|
||||
"fo"
|
||||
|
||||
"""
|
||||
|
||||
use GenServer.Behaviour
|
||||
|
||||
@doc """
|
||||
Creates an IO device.
|
||||
|
||||
If the `:capture_prompt` option is set to `true`,
|
||||
prompts (specified as arguments to `IO.get*` functions)
|
||||
are captured.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("foo")
|
||||
iex> IO.gets(pid, ">")
|
||||
"foo"
|
||||
iex> StringIO.contents(pid)
|
||||
{"", ""}
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("foo", capture_prompt: true)
|
||||
iex> IO.gets(pid, ">")
|
||||
"foo"
|
||||
iex> StringIO.contents(pid)
|
||||
{"", ">"}
|
||||
|
||||
"""
|
||||
@spec open(binary, Keyword.t) :: {:ok, pid}
|
||||
def open(string, options \\ []) when is_binary(string) do
|
||||
:gen_server.start_link(__MODULE__, {string, options}, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns current buffers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("in")
|
||||
iex> IO.write(pid, "out")
|
||||
iex> StringIO.contents(pid)
|
||||
{"in", "out"}
|
||||
|
||||
"""
|
||||
@spec contents(pid) :: {binary, binary}
|
||||
def contents(pid) when is_pid(pid) do
|
||||
:gen_server.call(pid, :contents)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the IO device and returns remaining buffers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("in")
|
||||
iex> IO.write(pid, "out")
|
||||
iex> StringIO.close(pid)
|
||||
{:ok, {"in", "out"}}
|
||||
|
||||
"""
|
||||
@spec close(pid) :: {:ok, {binary, binary}}
|
||||
def close(pid) when is_pid(pid) do
|
||||
:gen_server.call(pid, :close)
|
||||
end
|
||||
|
||||
## callbacks
|
||||
|
||||
def init({string, options}) do
|
||||
capture_prompt = options[:capture_prompt] || false
|
||||
{:ok, %{input: string, output: "", capture_prompt: capture_prompt}}
|
||||
end
|
||||
|
||||
def handle_info({:io_request, from, reply_as, req}, s) do
|
||||
s = io_request(from, reply_as, req, s)
|
||||
{:noreply, s}
|
||||
end
|
||||
|
||||
def handle_info(msg, s) do
|
||||
super(msg, s)
|
||||
end
|
||||
|
||||
def handle_call(:contents, _from, %{input: input, output: output} = s) do
|
||||
{:reply, {input, output}, s}
|
||||
end
|
||||
|
||||
def handle_call(:close, _from, %{input: input, output: output} = s) do
|
||||
{:stop, :normal, {:ok, {input, output}}, s}
|
||||
end
|
||||
|
||||
def handle_call(request, from, s) do
|
||||
super(request, from, s)
|
||||
end
|
||||
|
||||
defp io_request(from, reply_as, req, s) do
|
||||
{reply, s} = io_request(req, s)
|
||||
io_reply(from, reply_as, to_reply(reply))
|
||||
s
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, chars}, %{output: output} = s) do
|
||||
{:ok, %{s | output: << output :: binary, String.from_char_data!(chars) :: binary >>}}
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, m, f, as}, %{output: output} = s) do
|
||||
chars = apply(m, f, as)
|
||||
{:ok, %{s | output: << output :: binary, String.from_char_data!(chars) :: binary >>}}
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, _encoding, chars}, s) do
|
||||
io_request({:put_chars, chars}, s)
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, _encoding, mod, func, args}, s) do
|
||||
io_request({:put_chars, mod, func, args}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_chars, prompt, n}, s) when n >= 0 do
|
||||
io_request({:get_chars, :latin1, prompt, n}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_chars, encoding, prompt, n}, s) when n >= 0 do
|
||||
get_chars(encoding, prompt, n, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_line, prompt}, s) do
|
||||
io_request({:get_line, :latin1, prompt}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_line, encoding, prompt}, s) do
|
||||
get_line(encoding, prompt, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_until, prompt, mod, fun, args}, s) do
|
||||
io_request({:get_until, :latin1, prompt, mod, fun, args}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_until, encoding, prompt, mod, fun, args}, s) do
|
||||
get_until(encoding, prompt, mod, fun, args, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_password, encoding}, s) do
|
||||
get_line(encoding, "", s)
|
||||
end
|
||||
|
||||
defp io_request({:setopts, _opts}, s) do
|
||||
{{:error, :enotsup}, s}
|
||||
end
|
||||
|
||||
defp io_request(:getopts, s) do
|
||||
{{:ok, [binary: true, encoding: :unicode]}, s}
|
||||
end
|
||||
|
||||
defp io_request({:get_geometry, :columns}, s) do
|
||||
{{:error, :enotsup}, s}
|
||||
end
|
||||
|
||||
defp io_request({:get_geometry, :rows}, s) do
|
||||
{{:error, :enotsup}, s}
|
||||
end
|
||||
|
||||
defp io_request({:requests, reqs}, s) do
|
||||
io_requests(reqs, {:ok, s})
|
||||
end
|
||||
|
||||
defp io_request(_, s) do
|
||||
{{:error, :request}, s}
|
||||
end
|
||||
|
||||
## get_chars
|
||||
|
||||
defp get_chars(encoding, prompt, n,
|
||||
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
|
||||
case do_get_chars(input, encoding, n) do
|
||||
{:error, _} = error ->
|
||||
{error, s}
|
||||
{result, input} ->
|
||||
if capture_prompt do
|
||||
output = << output :: binary, String.from_char_data!(prompt) :: binary >>
|
||||
end
|
||||
|
||||
{result, %{s | input: input, output: output}}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_chars("", _encoding, _n) do
|
||||
{:eof, ""}
|
||||
end
|
||||
|
||||
defp do_get_chars(input, :latin1, n) when byte_size(input) < n do
|
||||
{input, ""}
|
||||
end
|
||||
|
||||
defp do_get_chars(input, :latin1, n) do
|
||||
<< chars :: [ binary, size(n) ], rest :: binary >> = input
|
||||
{chars, rest}
|
||||
end
|
||||
|
||||
defp do_get_chars(input, encoding, n) do
|
||||
try do
|
||||
case :file_io_server.count_and_find(input, n, encoding) do
|
||||
{buf_count, split_pos} when buf_count < n or split_pos == :none ->
|
||||
{input, ""}
|
||||
{_buf_count, split_pos} ->
|
||||
<< chars :: [ binary, size(split_pos) ], rest :: binary >> = input
|
||||
{chars, rest}
|
||||
end
|
||||
catch
|
||||
:exit, :invalid_unicode ->
|
||||
{:error, :invalid_unicode}
|
||||
end
|
||||
end
|
||||
|
||||
## get_line
|
||||
|
||||
defp get_line(encoding, prompt,
|
||||
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
|
||||
case :unicode.characters_to_list(input, encoding) do
|
||||
{:error, _, _} ->
|
||||
{{:error, :collect_line}, s}
|
||||
{:incomplete, _, _} ->
|
||||
{{:error, :collect_line}, s}
|
||||
chars ->
|
||||
{result, input} = do_get_line(chars, encoding)
|
||||
|
||||
if capture_prompt do
|
||||
output = << output :: binary, String.from_char_data!(prompt) :: binary >>
|
||||
end
|
||||
|
||||
{result, %{s | input: input, output: output}}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_line('', _encoding) do
|
||||
{:eof, ""}
|
||||
end
|
||||
|
||||
defp do_get_line(chars, encoding) do
|
||||
{line, rest} = collect_line(chars)
|
||||
{:unicode.characters_to_binary(line, encoding),
|
||||
:unicode.characters_to_binary(rest, encoding)}
|
||||
end
|
||||
|
||||
## 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)
|
||||
|
||||
if capture_prompt do
|
||||
output = << output :: binary, :binary.copy(String.from_char_data!(prompt), count) :: binary >>
|
||||
end
|
||||
|
||||
input =
|
||||
case input do
|
||||
:eof -> ""
|
||||
_ -> :unicode.characters_to_binary(input, encoding)
|
||||
end
|
||||
|
||||
{result, %{s | input: input, output: output}}
|
||||
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
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_until(chars, encoding, mod, fun, args, continuation, count) do
|
||||
{line, rest} = collect_line(chars)
|
||||
|
||||
case apply(mod, fun, [continuation, line | args]) do
|
||||
{:done, result, rest1} ->
|
||||
unless rest1 == :eof do
|
||||
rest = rest1 ++ rest
|
||||
end
|
||||
{result, rest, count + 1}
|
||||
{:more, next_continuation} ->
|
||||
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
## io_requests
|
||||
|
||||
defp io_requests([r|rs], {:ok, s}) do
|
||||
io_requests(rs, io_request(r, s))
|
||||
end
|
||||
|
||||
defp io_requests(_, result) do
|
||||
result
|
||||
end
|
||||
|
||||
## helpers
|
||||
|
||||
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
|
||||
|
||||
defp to_reply(list) when is_list(list), do: String.from_char_data!(list)
|
||||
defp to_reply(other), do: other
|
||||
end
|
||||
@@ -1,184 +0,0 @@
|
||||
defmodule Supervisor.Behaviour do
|
||||
@moduledoc """
|
||||
This module is a convenience to define Supervisor
|
||||
callbacks in Elixir. By using this module, you get
|
||||
the module behaviour automatically tagged as
|
||||
`:supervisor` and some helper functions are imported
|
||||
to make defining supervisors easier.
|
||||
|
||||
For more information on supervisors, please check the
|
||||
remaining functions defined in this module or refer to
|
||||
the following:
|
||||
|
||||
http://www.erlang.org/doc/man/supervisor.html
|
||||
http://www.erlang.org/doc/design_principles/sup_princ.html
|
||||
http://learnyousomeerlang.com/supervisors
|
||||
|
||||
## Example
|
||||
|
||||
defmodule ExUnit.Sup do
|
||||
use Supervisor.Behaviour
|
||||
|
||||
def init(user_options) do
|
||||
tree = [ worker(ExUnit.Runner, [user_options]) ]
|
||||
supervise(tree, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, pid} = :supervisor.start_link(MyServer, [])
|
||||
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :supervisor
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of children (worker or supervisors) to
|
||||
supervise and a set of options. Returns a tuple containing
|
||||
the supervisor specification.
|
||||
|
||||
## Examples
|
||||
|
||||
supervise children, strategy: :one_for_one
|
||||
|
||||
## Options
|
||||
|
||||
* `:strategy` - the restart strategy option It can be either
|
||||
`:one_for_one`, `:rest_for_one`, `:one_for_all` and
|
||||
`:simple_one_for_one`;
|
||||
|
||||
* `:max_restarts` - the maximum amount of restarts allowed in
|
||||
a time frame. Defaults to 5;
|
||||
|
||||
* `:max_seconds` - the time frame in which max_restarts applies.
|
||||
Defaults to 5;
|
||||
|
||||
The `:strategy` option is required and by default maximum 5 restarts
|
||||
are allowed in 5 seconds.
|
||||
|
||||
## Strategies
|
||||
|
||||
* `:one_for_one` - If a child process terminates, only that
|
||||
process is restarted;
|
||||
|
||||
* `:one_for_all` - If a child process terminates, all other child
|
||||
processes are terminated and then all child processes, including
|
||||
the terminated one, are restarted;
|
||||
|
||||
* `:rest_for_one` - If a child process terminates, the "rest" of
|
||||
the child processes, i.e. the child processes after the terminated
|
||||
process in start order, are terminated. Then the terminated child
|
||||
process and the rest of the child processes are restarted;
|
||||
|
||||
* `:simple_one_for_one` - Similar to `:one_for_one` but suits better
|
||||
when dynamically attaching children;
|
||||
|
||||
"""
|
||||
def supervise(children, options) do
|
||||
unless strategy = options[:strategy] do
|
||||
raise ArgumentError, message: "expected :strategy option to be given to supervise"
|
||||
end
|
||||
|
||||
maxR = Keyword.get(options, :max_restarts, 5)
|
||||
maxS = Keyword.get(options, :max_seconds, 5)
|
||||
|
||||
{:ok, {{strategy, maxR, maxS}, children}}
|
||||
end
|
||||
|
||||
@child_doc """
|
||||
## Options
|
||||
|
||||
* `:id` - a name used to identify the child specification
|
||||
internally by the supervisor. Defaults to the module name;
|
||||
|
||||
* `:function` - the function to invoke on the child to start it.
|
||||
Defaults to `:start_link`;
|
||||
|
||||
* `:restart` - defines when the child process should restart.
|
||||
Defaults to `:permanent`;
|
||||
|
||||
* `:shutdown` - defines how a child process should be terminated.
|
||||
Defaults to `5000` for a worker and `:infinity` for a supervisor;
|
||||
|
||||
* `:modules` - it should be a list with one element `[module]`,
|
||||
where module is the name of the callback module only if the
|
||||
child process is a supervisor, `gen_server` or `gen_fsm`. If the
|
||||
child process is a gen_event, modules should be `:dynamic`.
|
||||
Defaults to a list with the given module;
|
||||
|
||||
## Restart values
|
||||
|
||||
The following restart values are supported:
|
||||
|
||||
* `:permanent` - the child process is always restarted;
|
||||
|
||||
* `:temporary` - the child process is never restarted (not even
|
||||
when the supervisor's strategy is `:rest_for_one` or `:one_for_all`);
|
||||
|
||||
* `:transient` - the child process is restarted only if it
|
||||
terminates abnormally, i.e. with another exit reason than
|
||||
`:normal`, `:shutdown` or `{:shutdown, term}`;
|
||||
|
||||
## Shutdown values
|
||||
|
||||
The following shutdown values are supported:
|
||||
|
||||
* `:brutal_kill` - the child process is unconditionally terminated
|
||||
using `exit(child, :kill)`;
|
||||
|
||||
* `:infinity` - if the child process is a supervisor, it is a mechanism
|
||||
to give the subtree enough time to shutdown. It can also be used with
|
||||
workers with care;
|
||||
|
||||
* Finally, it can also be any integer meaning that the supervisor tells
|
||||
the child process to terminate by calling `exit(child, :shutdown)` and
|
||||
then waits for an exit signal back. If no exit signal is received within
|
||||
the specified time (in miliseconds), the child process is unconditionally
|
||||
terminated using `exit(child, :kill)`;
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Defines the given `module` as a worker which will be started
|
||||
with the given arguments.
|
||||
|
||||
worker ExUnit.Runner, [], restart: :permanent
|
||||
|
||||
By default, the function `:start_link` is invoked on the given module.
|
||||
|
||||
#{@child_doc}
|
||||
"""
|
||||
def worker(module, args, options \\ []) do
|
||||
child(:worker, module, args, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines the given `module` as a supervisor which will be started
|
||||
with the given arguments.
|
||||
|
||||
supervisor ExUnit.Runner, [], restart: :permanent
|
||||
|
||||
By default, the function `:start_link` is invoked on the given module.
|
||||
|
||||
#{@child_doc}
|
||||
"""
|
||||
def supervisor(module, args, options \\ []) do
|
||||
options = Keyword.update(options, :shutdown, :infinity, fn(x) -> x end)
|
||||
child(:supervisor, module, args, options)
|
||||
end
|
||||
|
||||
defp child(type, module, args, options) do
|
||||
id = Keyword.get(options, :id, module)
|
||||
modules = Keyword.get(options, :modules, [module])
|
||||
function = Keyword.get(options, :function, :start_link)
|
||||
restart = Keyword.get(options, :restart, :permanent)
|
||||
shutdown = Keyword.get(options, :shutdown, 5000)
|
||||
|
||||
{id, {module, function, args},
|
||||
restart, shutdown, type, modules}
|
||||
end
|
||||
end
|
||||
@@ -1,390 +0,0 @@
|
||||
defmodule System do
|
||||
@moduledoc """
|
||||
The System module provides access to variables used or
|
||||
maintained by the VM and to functions that interact directly
|
||||
with the VM or the host system.
|
||||
"""
|
||||
|
||||
defp strip_re(iodata, pattern) do
|
||||
:re.replace(iodata, pattern, "", [return: :binary])
|
||||
end
|
||||
|
||||
defp read_stripped(path) do
|
||||
case :file.read_file(path) do
|
||||
{:ok, binary} ->
|
||||
strip_re(binary, "^\s+|\s+$")
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
# Read and strip the version from the `VERSION` file.
|
||||
defmacrop get_version do
|
||||
case read_stripped(:filename.join(__DIR__, "../../../VERSION")) do
|
||||
"" -> raise RuntimeError, message: "could not read the version number from VERSION"
|
||||
data -> data
|
||||
end
|
||||
end
|
||||
|
||||
# Tries to run `git describe --always --tags`. In the case of success returns
|
||||
# the most recent tag. If that is not available, tries to read the commit hash
|
||||
# from .git/HEAD. If that fails, returns an empty string.
|
||||
defmacrop get_describe do
|
||||
dirpath = :filename.join(__DIR__, "../../../.git")
|
||||
case :file.read_file_info(dirpath) do
|
||||
{:ok, _} ->
|
||||
if :os.find_executable('git') do
|
||||
data = :os.cmd('git describe --always --tags')
|
||||
strip_re(data, "\n")
|
||||
else
|
||||
read_stripped(:filename.join(".git", "HEAD"))
|
||||
end
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
# Get the date at compilation time.
|
||||
defmacrop get_date do
|
||||
iodata_to_binary :httpd_util.rfc1123_date
|
||||
end
|
||||
|
||||
@doc """
|
||||
Elixir version information.
|
||||
|
||||
Returns Elixir's version as binary.
|
||||
"""
|
||||
@spec version() :: String.t
|
||||
def version, do: get_version
|
||||
|
||||
@doc """
|
||||
Elixir build information.
|
||||
|
||||
Returns a keyword list with Elixir version, git tag info and compilation date.
|
||||
"""
|
||||
@spec build_info() :: Keyword.t
|
||||
def build_info do
|
||||
[version: version, tag: get_describe, date: get_date]
|
||||
end
|
||||
|
||||
@doc """
|
||||
List command line arguments.
|
||||
|
||||
Returns the list of command line arguments passed to the program.
|
||||
"""
|
||||
@spec argv() :: [String.t]
|
||||
def argv do
|
||||
:elixir_code_server.call :argv
|
||||
end
|
||||
|
||||
@doc """
|
||||
Modify command line arguments.
|
||||
|
||||
Changes the list of command line arguments. Use it with caution,
|
||||
as it destroys any previous argv information.
|
||||
"""
|
||||
@spec argv([String.t]) :: :ok
|
||||
def argv(args) do
|
||||
:elixir_code_server.cast({:argv, args})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Current working directory.
|
||||
|
||||
Returns the current working directory or `nil` if one
|
||||
is not available.
|
||||
"""
|
||||
def cwd do
|
||||
case :file.get_cwd do
|
||||
{:ok, base} -> String.from_char_data!(base)
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Current working directory, exception on error.
|
||||
|
||||
Returns the current working directory or raises `RuntimeError`.
|
||||
"""
|
||||
def cwd! do
|
||||
cwd ||
|
||||
raise RuntimeError, message: "could not get a current working directory, the current location is not accessible"
|
||||
end
|
||||
|
||||
@doc """
|
||||
User home directory.
|
||||
|
||||
Returns the user home directory (platform independent).
|
||||
Returns `nil` if no user home is set.
|
||||
"""
|
||||
def user_home do
|
||||
case :os.type() do
|
||||
{:win32, _} -> get_windows_home
|
||||
_ -> get_unix_home
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
User home directory, exception on error.
|
||||
|
||||
Same as `user_home/0` but raises `RuntimeError`
|
||||
instead of returning `nil` if no user home is set.
|
||||
"""
|
||||
def user_home! do
|
||||
user_home ||
|
||||
raise RuntimeError, message: "could not find the user home, please set the HOME environment variable"
|
||||
end
|
||||
|
||||
defp get_unix_home do
|
||||
get_env("HOME")
|
||||
end
|
||||
|
||||
defp get_windows_home do
|
||||
:filename.absname(
|
||||
get_env("USERPROFILE") || (
|
||||
hd = get_env("HOMEDRIVE")
|
||||
hp = get_env("HOMEPATH")
|
||||
hd && hp && hd <> hp
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Writable temporary directory.
|
||||
|
||||
Returns a writable temporary directory.
|
||||
Searches for directories in the following order:
|
||||
|
||||
1. The directory named by the TMPDIR environment variable
|
||||
2. The directory named by the TEMP environment variable
|
||||
3. The directory named by the TMP environment variable
|
||||
4. `C:\TMP` on Windows or `/tmp` on Unix
|
||||
5. As a last resort, the current working directory
|
||||
|
||||
Returns `nil` if none of the above are writable.
|
||||
"""
|
||||
def tmp_dir do
|
||||
write_env_tmp_dir('TMPDIR') ||
|
||||
write_env_tmp_dir('TEMP') ||
|
||||
write_env_tmp_dir('TMP') ||
|
||||
write_tmp_dir('/tmp') ||
|
||||
((cwd = cwd()) && write_tmp_dir(cwd))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writable temporary directory, exception on error.
|
||||
|
||||
Same as `tmp_dir/0` but raises `RuntimeError`
|
||||
instead of returning `nil` if no temp dir is set.
|
||||
"""
|
||||
def tmp_dir! do
|
||||
tmp_dir ||
|
||||
raise RuntimeError, message: "could not get a writable temporary directory, " <>
|
||||
"please set the TMPDIR environment variable"
|
||||
end
|
||||
|
||||
defp write_env_tmp_dir(env) do
|
||||
case :os.getenv(env) do
|
||||
false -> nil
|
||||
tmp -> write_tmp_dir(tmp)
|
||||
end
|
||||
end
|
||||
|
||||
defp write_tmp_dir(dir) do
|
||||
case File.stat(dir) do
|
||||
{:ok, stat} ->
|
||||
case {stat.type, stat.access} do
|
||||
{:directory, access} when access in [:read_write, :write] ->
|
||||
String.from_char_data!(dir)
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
{:error, _} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Register a program exit handler function.
|
||||
|
||||
Registers a function that will be invoked
|
||||
at the end of program execution. Useful for
|
||||
invoking a hook in "script" mode.
|
||||
|
||||
The function must receive the exit status code
|
||||
as an argument.
|
||||
"""
|
||||
def at_exit(fun) when is_function(fun, 1) do
|
||||
:elixir_code_server.cast {:at_exit, fun}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Execute a system command.
|
||||
|
||||
Executes `command` in a command shell of the target OS,
|
||||
captures the standard output of the command and returns
|
||||
the result as a binary.
|
||||
|
||||
If `command` is a char list, a char list is returned.
|
||||
Otherwise a string, correctly encoded in UTF-8, is expected.
|
||||
"""
|
||||
@spec cmd(String.t) :: String.t
|
||||
@spec cmd(char_list) :: char_list
|
||||
|
||||
def cmd(command) when is_list(command) do
|
||||
:os.cmd(command)
|
||||
end
|
||||
|
||||
def cmd(command) when is_binary(command) do
|
||||
String.from_char_data! :os.cmd(List.from_char_data!(command))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Locate an executable on the system.
|
||||
|
||||
This function looks up an executable program given
|
||||
its name using the environment variable PATH on Unix
|
||||
and Windows. It also considers the proper executable
|
||||
extension for each OS, so for Windows it will try to
|
||||
lookup files with `.com`, `.cmd` or similar extensions.
|
||||
|
||||
If `program` is a char list, a char list is returned.
|
||||
Returns a binary otherwise.
|
||||
"""
|
||||
@spec find_executable(binary) :: binary | nil
|
||||
@spec find_executable(char_list) :: char_list | nil
|
||||
|
||||
def find_executable(program) when is_list(program) do
|
||||
:os.find_executable(program) || nil
|
||||
end
|
||||
|
||||
def find_executable(program) when is_binary(program) do
|
||||
case :os.find_executable(List.from_char_data!(program)) do
|
||||
false -> nil
|
||||
other -> String.from_char_data!(other)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
System environment variables.
|
||||
|
||||
Returns a list of all environment variables. Each variable is given as a
|
||||
`{name, value}` tuple where both `name` and `value` are strings.
|
||||
"""
|
||||
@spec get_env() :: %{String.t => String.t}
|
||||
def get_env do
|
||||
Enum.into(:os.getenv, %{}, fn var ->
|
||||
var = String.from_char_data! var
|
||||
[k, v] = String.split var, "=", parts: 2
|
||||
{k, v}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Environment variable value.
|
||||
|
||||
Returns the value of the environment variable
|
||||
`varname` as a binary, or `nil` if the environment
|
||||
variable is undefined.
|
||||
"""
|
||||
@spec get_env(binary) :: binary | nil
|
||||
def get_env(varname) when is_binary(varname) do
|
||||
case :os.getenv(List.from_char_data!(varname)) do
|
||||
false -> nil
|
||||
other -> String.from_char_data!(other)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Erlang VM process identifier.
|
||||
|
||||
Returns the process identifier of the current Erlang emulator
|
||||
in the format most commonly used by the operating system environment.
|
||||
|
||||
See http://www.erlang.org/doc/man/os.html#getpid-0 for more info.
|
||||
"""
|
||||
@spec get_pid() :: binary
|
||||
def get_pid, do: iodata_to_binary(:os.getpid)
|
||||
|
||||
@doc """
|
||||
Set an environment variable value.
|
||||
|
||||
Sets a new `value` for the environment variable `varname`.
|
||||
"""
|
||||
@spec put_env(binary, binary) :: :ok
|
||||
def put_env(varname, value) when is_binary(varname) and is_binary(value) do
|
||||
:os.putenv List.from_char_data!(varname), List.from_char_data!(value)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set multiple environment variables.
|
||||
|
||||
Sets a new value for each environment variable corresponding
|
||||
to each key in `dict`.
|
||||
"""
|
||||
@spec put_env(Dict.t) :: :ok
|
||||
def put_env(dict) do
|
||||
Enum.each dict, fn {key, val} -> put_env key, val end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes an environment variable.
|
||||
|
||||
Removes the variable `varname` from the environment.
|
||||
"""
|
||||
@spec delete_env(String.t) :: :ok
|
||||
def delete_env(varname) do
|
||||
:os.unsetenv(List.from_char_data!(varname))
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Last exception stacktrace.
|
||||
|
||||
Note that the Erlang VM (and therefore this function) does not
|
||||
return the current stacktrace but rather the stacktrace of the
|
||||
latest exception.
|
||||
|
||||
Inlined by the compiler into `:erlang.get_stacktrace/0`.
|
||||
"""
|
||||
def stacktrace do
|
||||
:erlang.get_stacktrace
|
||||
end
|
||||
|
||||
@doc """
|
||||
Halt the Erlang runtime system.
|
||||
|
||||
Halts the Erlang runtime system where the argument `status` must be a
|
||||
non-negative integer, the atom `:abort` or a binary.
|
||||
|
||||
* If an integer, the runtime system exits with the integer value which
|
||||
is returned to the operating system;
|
||||
|
||||
* If `:abort`, the runtime system aborts producing a core dump, if that is
|
||||
enabled in the operating system;
|
||||
|
||||
* If a string, an erlang crash dump is produced with status as slogan,
|
||||
and then the runtime system exits with status code 1;
|
||||
|
||||
Note that on many platforms, only the status codes 0-255 are supported
|
||||
by the operating system.
|
||||
|
||||
For more information, check: http://www.erlang.org/doc/man/erlang.html#halt-1
|
||||
|
||||
## Examples
|
||||
|
||||
System.halt(0)
|
||||
System.halt(1)
|
||||
System.halt(:abort)
|
||||
|
||||
"""
|
||||
@spec halt() :: no_return
|
||||
@spec halt(non_neg_integer | binary | :abort) :: no_return
|
||||
def halt(status \\ 0)
|
||||
|
||||
def halt(status) when is_integer(status) or status == :abort do
|
||||
:erlang.halt(status)
|
||||
end
|
||||
|
||||
def halt(status) when is_binary(status) do
|
||||
:erlang.halt(List.from_char_data!(status))
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
defmodule Tuple do
|
||||
@moduledoc """
|
||||
Functions for working with tuples.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Create a new tuple.
|
||||
|
||||
Creates a tuple of size `size` containing the
|
||||
given `data` at every position.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Tuple.duplicate(:hello, 3)
|
||||
{:hello, :hello, :hello}
|
||||
|
||||
"""
|
||||
@spec duplicate(term, non_neg_integer) :: tuple
|
||||
def duplicate(data, size) do
|
||||
:erlang.make_tuple(size, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Insert an element into a tuple.
|
||||
|
||||
Inserts `value` into `tuple` at the given zero-based `index`.
|
||||
Raises an `ArgumentError` if `index` is greater than the
|
||||
length of `tuple`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> tuple = {:bar, :baz}
|
||||
iex> Tuple.insert_at(tuple, 0, :foo)
|
||||
{:foo, :bar, :baz}
|
||||
|
||||
"""
|
||||
@spec insert_at(tuple, non_neg_integer, term) :: tuple
|
||||
def insert_at(tuple, index, term) do
|
||||
:erlang.insert_element(index + 1, tuple, term)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove an element from a tuple.
|
||||
|
||||
Deletes the element at the zero-based `index` from `tuple`.
|
||||
Raises an `ArgumentError` if `index` is greater than
|
||||
or equal to the length of `tuple`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> tuple = {:foo, :bar, :baz}
|
||||
iex> Tuple.delete_at(tuple, 0)
|
||||
{:bar, :baz}
|
||||
|
||||
"""
|
||||
@spec delete_at(tuple, non_neg_integer) :: tuple
|
||||
def delete_at(tuple, index) do
|
||||
:erlang.delete_element(index + 1, tuple)
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user