Compare commits

...
641 Commits
Author SHA1 Message Date
José Valim 5c5259826f Release v1.11.1 2020-10-16 09:31:35 +02:00
Xavier Noria ccdefd904f Improves the docs of config/runtime.exs and Config.config_env/0 (#10414) 2020-10-16 09:23:13 +02:00
José Valim ff95a59ec0 Add :append to stream_mode, closes #10421 2020-10-16 09:22:44 +02:00
José Valim 4081590742 Add inet6 fallback to Mix usage of httpc, closes #10423 2020-10-16 09:22:16 +02:00
José Valim 693df5fe8c Properly handle tags inside typespec in Erlang docs, closes #10430 2020-10-16 09:22:11 +02:00
José Valim b4605374b5 Do not deadlock Logger if handler crashes on sync mode, closes #10420 2020-10-12 09:27:32 +02:00
Eksperimental 36ec08e0c8 Fix warning message in app requirement (#10417)
It was missing a closing square bracket.

Previously it was saying

  3. In case you don't want to add a requirement to :crypto, you may optionally skip this warning by adding [xref: [exclude: :crypto] to your "def project" in mix.exs

Now

  3. In case you don't want to add a requirement to :crypto, you may optionally skip this warning by adding [xref: [exclude: :crypto]] to your "def project" in mix.exs
2020-10-11 00:15:59 +02:00
José Valim ae0ac9b3c9 Ignore tracers if lexical tracker is dead or explicitly nil 2020-10-11 00:15:41 +02:00
Milton Mazzarri e3479831ba Do not show warning when super is in GenServer.child_spec/1 (#10418)
The original goal[1] was to deprecate the usage of `super` on every `GenServer`
callback, but `child_spec/1` is not a callback, it's a default implementation
that can be re-implemented.

This commit removes the _deprecation warning_ when someone use `super` in
`GenServer.child_spec/1`.

Fixes: #10415

[1] https://github.com/elixir-lang/elixir/commit/c024b0eeb2dc69a759fe5ceeb1c8216d58ae9386#diff-ed0344c13ed25389035f337d94902894R301-R316
2020-10-11 00:15:40 +02:00
José Valim ffbcc004ec Do not crash when :reduce is set to nil in comprehensions, closes #10411 2020-10-08 15:37:53 +02:00
José Valim e0d61c7875 Fix order dependent tests in logger suite 2020-10-08 09:43:51 +02:00
Michał Łępicki b2059031a6 Fix returning errors in Module.Types.Expr.of_expr (#10410) 2020-10-08 09:37:13 +02:00
José Valim ed2829d8ea Keep warnings when traversing clauses 2020-10-07 23:53:23 +02:00
José Valim 081f99cd37 Consider targets when filtering apps, closes #10405 2020-10-06 19:31:36 +02:00
José Valim 7837cc5c59 Release v1.11.0 2020-10-06 15:43:09 +02:00
Daniil Fedotov b96fc39422 Restrict Logger.configure on config_change callback (#10388)
The `config_change` callback is called from `application_controller` process,
which is receiving env updates on releases. `Logger.configure` does a blocking
call to the `Logger` process, which calls `Application.put_env`

This causes a crash on logger config update.

The fix is to call `Logger.configure` only for the config that have not yet
been persisted.
2020-10-03 08:33:31 +02:00
Waldemar Rachwał e1cb898198 Take --dot-iex into account after shell respawn (#10384) 2020-10-01 20:36:14 +02:00
Dave Lucia 88e2894925 Enum.max_by/3 and Enum.min_by/3 dialyzer fixes (#10383) 2020-10-01 13:23:19 +02:00
José Valim f7cb5ae053 Fix inference of open maps with non-singleton keys
Closes #10371.
2020-09-29 13:11:22 +02:00
José Valim b4845e6854 Update iso.ex 2020-09-25 13:34:38 +02:00
José Valim 0f1200f99b Add a note on async_stream+take 2020-09-25 13:33:15 +02:00
José Valim cec1a7190b Make config/runtime.exs deep merge, closes #10365 2020-09-25 13:32:54 +02:00
Gabriel Alves 53d1b98a05 Raise instead of crashing on when trying to use list operators in guards (#10363) 2020-09-23 14:30:39 +02:00
José Valim 4828f11cd2 Keep invoking loadconfig on cli boot 2020-09-22 15:47:35 +02:00
José Valim 501271cad0 Make replace and replace! in Keyword consistent, closes #10345 2020-09-18 16:13:16 +02:00
José Valim fd4a6ad5cf Handle empty map in other places, closes #10343 2020-09-18 10:53:56 +02:00
José Valim 196aab87ca Align doc chunks return with EEP 48 2020-09-18 10:41:39 +02:00
José Valim 59b266506a Append space on middle and start expr if necessary, closes #10340 2020-09-17 17:11:28 +02:00
José Valim 1c8e625d77 Speed up compilation with multiple describe 2020-09-17 14:24:54 +02:00
José Valim f9d7adbfdf Introduce register_test/6 to speed up test definition 2020-09-17 10:57:00 +02:00
José Valim f1721c8a58 Reduce amount of injected code in case 2020-09-17 10:29:40 +02:00
José Valim 57abca4c16 Properly handle reduce in types, closes #10224 2020-09-14 23:02:07 +02:00
José Valim 8e9ece0d62 More typing improvements (#10333)
1. Move push_expr_stack to unify and keep helpers typing free

2. Add get_var! when accessing variables so it helps us find
   variables that have not been properly processed

3. Properly process all variables found missing on step 2
2020-09-14 22:45:34 +02:00
José Valim 54ba096d34 Remove boolean and number as builtin types (#10329) 2020-09-14 22:45:34 +02:00
José Valim 6042fd0bcc Infer -> Unify 2020-09-14 22:45:34 +02:00
Eksperimental 475b73b46f Use proper casing in CHANGELOG (#10331) 2020-09-14 15:24:51 +02:00
José Valim 3cf6069ad9 Fix typo in CHANGELOG 2020-09-14 09:43:19 +02:00
José Valim 48cee5aaa5 Release v1.11.0-rc.0 2020-09-14 09:16:34 +02:00
Adriano Mitre 94160c0ed1 Add "since" doc metadata to File.rename/2 (#10328) 2020-09-14 09:08:58 +02:00
José Valim bc542dcc2a Hoist expressions when creating records, closes #10313 2020-09-13 12:27:59 +02:00
José Valim 5e5f00c6fe Document all built-in types internally (#10327)
Also document which functions must be changed once
new types are introduced and which functions must
handle them.

Make sure all of these functions comply to the spec
and improve performance for tuples.
2020-09-13 12:22:38 +02:00
Eric Meadows-Jönsson a59fe07529 Move type warnings (#10324)
No longer needs to compile modules to test type warnings. Reduces type tests execution time from 1.0s to 0.6s.
2020-09-13 10:48:51 +02:00
José Valim 0c3aebbc47 Handle subtypes in map keys (#10323) 2020-09-13 09:41:48 +02:00
Saša Jurić e7555da79f Fix unregistration crash (#10326) 2020-09-13 09:32:37 +02:00
Adriano Mitre 98cfa7b738 add "since" doc metadata to System.otp_release/0 (#10325) 2020-09-12 22:19:48 +02:00
Eksperimental 556bd254db Improve wording in Map module (#10318) 2020-09-12 09:08:14 +02:00
sabiwara 4b9a2bb8dc Improve links in pattern matching page (#10322) 2020-09-12 09:05:39 +02:00
José Valim 3a876ca053 Update CHANGELOG 2020-09-11 23:33:45 +02:00
Daniel Cukier 3ab4623657 Added ignore module option to coverage report (#10317) 2020-09-11 20:45:30 +02:00
Eksperimental 5d9eb80874 Fix small typo in comment (#10321) 2020-09-11 20:00:53 +02:00
José Valim 8167bb65d1 Move struct handling from unify to AST (#10320)
We also improve error messages to list all
available fields and introduce Module.Types.Of
module to hold shared functionality between Expr
and Pattern.
2020-09-11 19:51:46 +02:00
nia ec8cde34d0 Use 'gmake' instead of 'make' when compiling deps on NetBSD/DragonFlyBSD (#10319)
There are basically two implementations of BSD make now, the
NetBSD one (also used by FreeBSD, available as 'bmake' on Linux),
and the OpenBSD one.
2020-09-11 18:27:40 +02:00
José Valim 0d18ecd286 Finish tidying up tests 2020-09-11 13:06:21 +02:00
José Valim 301711af12 Add map test and shared type helper 2020-09-11 12:29:37 +02:00
Eric Meadows-Jönsson cc9f4b8292 Check map updates (#10316) 2020-09-11 11:46:48 +02:00
José Valim b52ddb30e0 Move Checker into ParallelChecker 2020-09-10 20:04:10 +02:00
José Valim d65994921a Run formatter 2020-09-10 17:39:57 +02:00
José Valim 42e24569f4 Move of_head to Pattern 2020-09-10 17:32:58 +02:00
José Valim 2de183ede6 Clear out patterns from types_test.exs and use aliases 2020-09-10 16:38:16 +02:00
José Valim cce861eec7 Improve docs and move all types tests to a single directory 2020-09-10 15:59:47 +02:00
José Valim 9b388a58a1 Reuse logic on exception blame and types errors 2020-09-10 15:46:09 +02:00
José Valim 29858c9d1e Only compute error if necessary 2020-09-10 15:46:09 +02:00
Eric Meadows-Jönsson 2a53036053 Fix trace tagging of internal type variable 2020-09-10 12:58:41 +02:00
Eric Meadows-Jönsson ef0f17dfe5 Improve map unification (#10310) 2020-09-10 12:37:43 +02:00
José Valim 1a6667d551 Improve doctests docs
Closes #10314.
2020-09-10 10:21:06 +02:00
sabiwara 1fca074184 Fix &//2 parsing issue (#10312) 2020-09-10 09:20:07 +02:00
José Valim 749459a32f Merge remote with inference pass (#10305) 2020-09-08 09:08:44 +02:00
Gustavo Saiani af11c51b72 Fix typo in Diff doc (#10304) 2020-09-07 14:25:55 +02:00
José Valim df1d9a6b90 Refactoring 2020-09-06 19:13:06 +02:00
José Valim 8f95fdadaf Introduce a type hint system (#10303)
Instead of trying to assuming that a certain usage of a type is the correct one, we keep the unification message agnostic and add hints to the message.

Each message may have multiple hints, as they are computed alongside the trace. The traces are also reversed in order to keep the order they are added.

Here is how the new warning for dot access look like:

    warning: incompatible types:

        map() !~ atom()

    in expression:

        # a.ex:4
        module.__struct__

    where "module" was given the type map() (due to calling var.field) in:

        # a.ex:4
        module.__struct__

    where "module" was given the type atom() in:

        # a.ex:3
        %module{}

    HINT: "var.field" (without parentheses) implies "var" is a map() while
    "var.fun()" (with parentheses) implies "var" is an atom()

    Conflict found at
      a.ex:4: A.a/1

And here is the hint for binaries:

    warning: incompatible types:

        binary() !~ integer()

    in expression:

        # a.ex:2
        <<foo>>

    where "foo" was given the type binary() in:

        # a.ex:2
        is_binary(foo)

    where "foo" was given the type integer() in:

        # a.ex:2
        <<foo>>

    HINT: all expressions given to binaries are assumed to be of type
    integer unless said otherwise. For example, <<expr>> assumes "expr" 
    is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, 
    to change the default behaviour.

    Conflict found at
      a.ex:2: A.a/1
2020-09-06 16:58:23 +02:00
Jason Axelson cf9fa416f8 Document atom comparison (#10302)
Currently how atoms are compared is not described in the Elixir docs.

Sentence is copied from the erlang docs:
https://erlang.org/doc/reference_manual/expressions.html#term-comparisons
2020-09-05 09:11:39 +02:00
José Valim 67cbd59d03 Remove reference to unknown function 2020-09-03 15:36:28 +02:00
José Valim 76bfcfa871 Simplify metadata operation 2020-09-03 13:01:33 +02:00
José Valim bc5bb85a17 reboot_system_after_config is opt-in if using config/runtime.exs 2020-09-03 12:59:43 +02:00
Enrico Rivarola 7eb29f5905 Include Erlang severity level to Logger metadata (#10300) 2020-09-03 09:48:13 +02:00
victor felder bad2b253d7 Add Registry.delete_meta/2 (#10298) 2020-09-03 09:07:17 +02:00
José Valim 8e20b02a71 Update CHANGELOG 2020-09-02 16:36:31 +02:00
Benjamin Milde 85910e7385 Update CHANGELOG.md (#10297) 2020-09-02 16:24:22 +02:00
Tajinder Chumber f6c67c3d49 Mention right associativity of the --/2 operator in docs (#10294) 2020-09-02 10:23:44 +02:00
José Valim 4019f7acf1 Add note about how new log levels are reported in Elixir backends 2020-09-01 18:11:10 +02:00
Eksperimental 095705d8e3 Correct title for Mix aliases section (#10293) 2020-08-31 20:33:02 +02:00
Michał Łępicki 8df88f9ac2 Pass code location to :logger.macro_log/4 (#10287) 2020-08-29 08:51:55 +02:00
Schnittchen 94b1df3ef4 Change link in "Protocols" section in Kernel docs (#10288)
Previously linking to `defprotocol/2` which contains no useful information and links to `Protocol`. Save reader one click.
2020-08-28 14:59:57 +02:00
Thomas Depierre 607110b39b Add data structure info to first arg in Protocol (#10283) 2020-08-25 18:54:45 +02:00
José Valim 2d9762f3f5 Use to_string chars on Logger.Formatter 2020-08-25 08:23:50 +02:00
José Valim 46ff8ccbc4 Improve error message 2020-08-22 20:46:40 +02:00
José Valim 273a929001 Document metadata used by Task, closes #10203 2020-08-22 20:46:40 +02:00
pini cf9ddd9194 Compare to test coverage threshold inclusively (#10275) (#10276)
Closes #10275
2020-08-21 14:50:59 +02:00
Vinicius Brasil 82dca5537a Raise CompileError when trying to define reserved types (#10274)
Closes #10263
2020-08-21 08:01:20 +02:00
Eksperimental 6c327d025a Use length instead of len in arguments and var names (#10273) 2020-08-20 19:26:20 +02:00
Austin Gatlin 65bf11d51f Minor grammar edits in Mix.Config (#10272) 2020-08-18 22:46:07 +02:00
José Valim e3344123c2 Do not propagate tracers to dynamic definitions, closes #10271 2020-08-18 20:06:44 +02:00
José Valim 1995478163 Make sure --label property apply without sources/sinks 2020-08-18 13:05:34 +02:00
José Valim 74f09347d5 Allow both sink and source to be given to mix xref graph 2020-08-18 12:59:22 +02:00
José Valim 82c4c3616b Deprecate Exception.exception?/1 and Regex.regex?/1 2020-08-17 09:54:13 +02:00
José Valim 2d6e7a6364 Expand ordering explanation 2020-08-17 09:25:39 +02:00
Vini Brasil f11ca077ec Add since doc to String#myers_difference/2 (#10268) 2020-08-15 15:07:22 +02:00
Adriano Mitre 0b51e0129e Make child specification docs more accurate (#10267) 2020-08-15 10:29:05 +02:00
Todd Resudek 0914d1d312 Add @since to System.os_time, System.schedulers and System.schedulers_online (#10266) 2020-08-14 21:38:00 +02:00
José Valim 403a5c1725 Also touch root manifest on will_recompile for umbrellas
This solves an issue where compile.protocols would not
force recompile when executed at the umbrella root.
2020-08-14 15:47:21 +02:00
Dominika a0cef3bdd7 Update string_to_quoted spec (#10265) 2020-08-14 14:54:14 +02:00
Eksperimental df449805c8 Revise use of Note and Notice verbs (#10264) 2020-08-14 07:59:34 +02:00
Eksperimental bc73ad46ef Mention missing required key syntax in Typespecs page (#10261) 2020-08-13 09:47:07 +02:00
José Valim e1e8f9afec Add column information to SyntaxError and TokenMissingError 2020-08-12 20:14:31 +02:00
Eksperimental 0f61583978 Fix typo in test name (#10258) 2020-08-12 16:31:35 +02:00
José Valim afa6d0c177 Add test for invalid tokenizer expressions 2020-08-12 14:18:13 +02:00
José Valim c260957de5 Store dot_iex_path when restarting the evaluator
Closes #10257.
2020-08-12 13:14:36 +02:00
José Valim 94fa4e5d19 Handle comments in EEx, closes #10256 2020-08-12 00:15:57 +02:00
marekzajac97 11ce4999c5 Fix not being able to escape "|" inside a table (docstring) (#10254) 2020-08-10 15:19:50 +02:00
Jeroen Visser 1c23624489 Fix since doc for mix cmd task (#10252) 2020-08-07 15:17:44 +02:00
José Valim d332c78e3b Improve docs for Kernel.var!, closes #10251 2020-08-06 08:05:40 +02:00
José Valim ff9c76c95a Do not crash when failing to eval/inspect structs, closes #10242 2020-08-02 17:14:41 +02:00
Tristan Sloughter 18515152d3 support rebar3 resource type git_subdir (#10246)
Related rebar3 PR which add git_subdir:
https://github.com/erlang/rebar3/pull/2239
2020-08-01 20:46:35 +02:00
Barna 70bcc1494d Make sentence about log levels more clear (#10243) 2020-07-31 14:10:16 +02:00
Łukasz Jan Niemier 3b80c1b1e0 Properly handle GenServer message in logger translators (#10209)
When the logger report was in form of `%{label: term(), report: term()}`
the `report_cb` wasn't called and instead there was fall-back to the
"regular" formatter which could cause meaningless messages. This fixes
that by falling back to the "proper" `report_cb` in case when there was
no matching handler.
2020-07-28 16:39:17 +02:00
Henrik Nyh ba1e45a08d Add Access.at! (#10193) 2020-07-28 16:38:17 +02:00
Eksperimental a19808942f Fix Code.Typespec.spec_to_quoted/2 when union in %{__struct__:} is used (#10238)
Originally reported in ExDoc.
https://github.com/elixir-lang/ex_doc/issues/1239

Code.Typespec.spec_to_quoted/2 wouldn't know how to deal with a
map with __struct__ key that is not a module, such as in:

@spec foo(%{__struct__: Foo | Bar}) :: any()
2020-07-28 16:37:41 +02:00
Eksperimental 4fa9076e52 Use Path.relative_to_cwd/1 in file path for slowest test (#10237) 2020-07-27 21:45:15 +02:00
Eksperimental 21f7a719d8 Sort t:ExUnit.state/0 fields alphabetically (#10236) 2020-07-27 20:48:24 +02:00
Wojtek Mach bfefb5590b Document when :file was added to ExUnit.TestModule (#10235) 2020-07-27 19:43:31 +02:00
Eksperimental 0fb465bcc6 Additional changes to ExUnit trace (#10234)
- Include file:line to slowest test results.
  Additionally it changes the order of the module, so it is consistent with the rest
  of the results.
- Print line number while test is being executed.
- Sort struct and spec fields alphabetically.

Related: fbfc8d2fd04c97c7fbfe7e35a482f76b67fbe3a4, #10232
2020-07-27 19:40:34 +02:00
José Valim 9a12013ed0 Add file and line to ExUnit --trace, closes #10232 2020-07-27 17:22:51 +02:00
Eksperimental cebfb83a7e Rename "initial" argument to "default" in Dic and HashDict (#10233)
To be consistent with recently introduced changes: #10228, #10223
2020-07-27 16:54:10 +02:00
José Valim 9cd30ce651 Update fetch_env application feedback, closes #10225 2020-07-27 09:50:59 +02:00
Diogo Magalhães Martins c025240fb0 Fix ambiguity on Path.expand/1 docs (#10231) 2020-07-27 09:32:57 +02:00
José Valim 65deef3907 Do not warn when configuring Elixir app
All of Elixir public configs work at runtime.
2020-07-26 19:28:15 +02:00
Eksperimental 0a0ff7fa9a Make spec clearer for update functions in Keywords (#10228)
Inspired by c1a538e4a8 (#10223)

* Fixes the spec of Keyword.get_and_update!/3 that was missing the :pop return value for the update function
* It removes the concept of the "get" value and replaced it consistently with "current value"
* Renames "initial" argument to "default"
* Uses "existing value" when applicable
2020-07-23 21:10:04 +02:00
José Valim 97e8b0242d Ensure reenabling a task from inside a child propagates up, closes #10227 2020-07-23 16:43:07 +02:00
KaFai afb6e207a4 Fix bug of only taking first app option in mix cmd (#10226) 2020-07-23 09:48:29 +02:00
José Valim 562fd7cc2c Conform www-url-encoded parsing to whatwg spec
See: https://url.spec.whatwg.org/#application/x-www-form-urlencoded
2020-07-23 09:27:29 +02:00
Eksperimental c1a538e4a8 Make spec clearer for update functions in Map (#10223)
* It removes the concept of the "get" value
* Renames "initial" argument to "default"
* Makes use of the concept of "current value" which was already present in some
  functions, and uses this in the examples and the specs consistently
2020-07-23 08:57:49 +02:00
Eksperimental 2013b4dc34 Improve Kernel documentation (#10222) 2020-07-22 20:22:58 +02:00
Oliver Severin Mulelid-Tynes 760cf2ccb6 Update Map.update example to use full lambda syntax on first example (#10221) 2020-07-22 19:08:30 +02:00
Eksperimental 512594c08a Mention file:line in Already defined as ... error message (#10219) 2020-07-22 14:59:37 +02:00
Eksperimental 660fc86ede More improvements to operator docs (#10220) 2020-07-22 14:44:12 +02:00
Eksperimental 7b27ffa7de Improvements to operator functions and page (#10218) 2020-07-22 07:59:52 +02:00
José Valim b7756f875c Remove email workflow now that CI was moved to GitHub 2020-07-21 11:01:02 +02:00
José Valim c56cc3df49 Fix suite 2020-07-21 10:22:55 +02:00
Eksperimental b0b0f8e0fd Improve keyword list definition and introduce the concept of key (#10215) 2020-07-21 09:56:31 +02:00
Eksperimental d38a9b8112 Clarify use of Enum.member?/2 in in/2 operator (#10216) 2020-07-21 09:55:40 +02:00
Eksperimental 72f70de5fd Improve Operators documentation (#10217)
- Use "less-than", "greater-than", and "equal to" operators. This the actual name used by Wikipedia and Unicode.
- Give a name to every operator in Kernel.
- Minor improvements here and there.
2020-07-21 09:54:44 +02:00
Eksperimental 5834b14433 Raise ArgumentError in Keyword.keys when list is not a keyword list (#10214)
Previously it would return something like this:

** (FunctionClauseError) no function clause matching in anonymous fn/1 in Keyword.keys/1

    The following arguments were given to anonymous fn/1 in Keyword.keys/1:

        # 1
        {"fetch/2", {:fetch, 2}}

Closes #10010, and it improves 3fd68ef62a
2020-07-21 09:52:29 +02:00
Eksperimental d0ec4296ba Small rewording in Supervisor.Spec (#10212) 2020-07-20 23:19:25 +02:00
Eksperimental 4822cb230c Fix typo in comment (#10211) 2020-07-20 23:06:58 +02:00
José Valim 323dac093b Consider == and === operators in ExUnit.Diff, closes #10210 2020-07-20 20:07:04 +02:00
Eksperimental 71503e07bd Update typespecs and enforce with and limit in Inspect.Algebra (#10205) 2020-07-18 17:01:28 +02:00
Łukasz Samson 983ee5c640 Make autocomplete work after pin and negation operators (#10207) 2020-07-18 10:47:20 +02:00
Eksperimental 2b6d3f8031 Fix canonical link in HTML docs (#10206)
The comments would add space to the CANONICAL variable,
redering the link like
<link rel="canonical" href="https://hexdocs.pm/elixir/v1.10/ /Kernel.html" />
2020-07-18 08:23:23 +02:00
José Valim 92cf894756 Require a trailing underscore to consider it a compiler variable 2020-07-17 13:59:52 +02:00
José Valim e798fd7762 Always load apps, even if --no-compile is given 2020-07-17 13:59:52 +02:00
Juha 854db25440 Fix markdown codeblock rendering issue (#10202) 2020-07-16 19:39:38 +02:00
José Valim ebc57cdd16 Revert "Add support for :hour and :minute time units (#10185)"
This reverts commit 35f6ff827c.
2020-07-16 18:35:45 +02:00
Fernando Tapia Rico fc6bf59462 Add List.Chars to bootstrap modules
If there is a compile error, Kernel.ParallelCompiler raises
`** (UndefinedFunctionError) function List.Chars.to_charlist/1 is
undefined (module List.Chars is not available)`.
2020-07-16 13:20:38 +02:00
Fernando Tapia Rico e764de29f6 Add MIX_BUILD_ROOT to config _build dir
The goal is to make it easier to change the path of the "_build"
directory.

Mix currently supports MIX_BUILD_PATH to customize where the build
artifacts are written to. One thing to keep in mind is that
MIX_BUILD_PATH expects the final path and it's up to the user to
use different paths per environment. MIX_BUILD_PATH is not
equivalent to the "_build" directory, it's equivalent to the
"_build/<env>" directory.

Mix also allows to configure the path of the "_build" directory
using the :build_path configuration of Mix.Project, but that is
meant to be used by umbrella applications:

    In a non-umbrella context, configuring this has undesirable
    side-effects (such as skipping some compiler checks) and
    should be avoided.

Finally, Mix.Project also uses :env_path to configure the build
path, but that option is not documented and it's marked as
"private" in the code.
2020-07-16 12:59:53 +02:00
Fernando Tapia Rico 480cbf66c3 Use GitHub Actions for Windows CI
Here are some benefits of moving to GitHub
Actions:

  * The Docker image `fertapric/elixir-ci:*` is
    no longer needed.

  * Build times has been reduced ~50%: current
    times are ~20 mins, and with GitHub Actions
    ~10 mins.

  * Jobs seem to be scheduled faster: a few
    seconds compared to minutes.

  * Running instances seem more stable: I have
    not experienced `ExUnit.TimeoutError` errors
    nor memory allocation issues.
2020-07-16 12:45:56 +02:00
José Valim 64afd6bebf Allow local deps to be skipped on mix deps.compile 2020-07-16 09:23:58 +02:00
Eric Meadows-Jönsson f80fedde81 Remaining map guards should add optional keys
Closes #10148.
2020-07-16 01:47:17 +02:00
José Valim 6228ade7d2 Do not warn if quotes are used on complex atoms
Atoms that have non-ascii characters or @ does not
require tokens. That's to streamline the developer
experience around examples :"foo@computer-name" and
:"hélló".
2020-07-15 20:10:31 +02:00
José Valim 08706cc490 Warn if a variable that looks like a compiler variable is unused (#10198)
Closes #10197.
2020-07-15 12:20:49 +02:00
Eksperimental 5e591ebfc6 Fix warnings detected by future version of ExDoc (#10195) 2020-07-15 12:20:19 +02:00
sabiwara 1c83baae63 Fix ExUnit format (#10196) 2020-07-15 11:13:38 +02:00
José Valim 4a7f19b97c Add t:ExUnit.test_id, closes #10192 2020-07-15 00:23:53 +02:00
Henrik Nyh 41159e562f Remove incorrect arities in test descriptions (#10194)
These tests actually use get_and_update_in/3 and pop_in/2.

I removed the arities, rather than correcting them, since it seems
like unnecessary detail here.
2020-07-15 00:07:04 +02:00
Parker Selbert 35f6ff827c Add support for :hour and :minute time units (#10185)
This enhances time unit aware functions in the `Time` module so that
they can operate on `:hour` and `:minute`, along with standard
`System.time_unit/0` values. This eliminates the need to convert all
values to seconds, which allows more expressive time manipulation.

Along with changes to `Time.add/3` and `Time.diff/3`, this adds
a new extended `time_unit` type to `Time`.
2020-07-14 20:34:20 +02:00
José Valim 295f4a7052 Load and clean tasks in two distinct steps, closes #10190 2020-07-14 18:18:01 +02:00
Eksperimental 98f2201e6d Use "bytecode" instead of "byte code" (#10189) 2020-07-14 15:44:58 +02:00
José Valim 5fa00e5260 Clarify to_unix docs 2020-07-14 11:46:34 +02:00
Eksperimental b04089d9b9 Enforce typespec limitation in bitstring unit (#10187) 2020-07-14 08:11:37 +02:00
Wojtek Mach 5196793ec3 Update syntax highlighting on CHANGELOG.md (#10183) 2020-07-13 15:37:17 +02:00
José Valim 9082f70a27 Update CHANGELOG 2020-07-13 12:48:11 +02:00
José Valim d135c7f8ca Add Date.day_of_week/2, beginning_of_week/2, and end_of_week/2 (#10181)
This augments the Calendar behaviour by adding
a new function, c:Calendar.day_of_week/4 which
supports a starting_on value with defaults and
calendar specific semantics.
2020-07-13 12:29:36 +02:00
Wojtek Mach 9f6e39d1b2 Update Date.range/2 to accept t:Calendar.date/0 (#10182) 2020-07-13 11:09:17 +02:00
Tomasz Marek Sulima c192083726 Add is_exception/1 and is_exception/2 guards (#10180) 2020-07-12 13:55:11 +02:00
José Valim 9413dcc573 Move logic for take over message to the server
This addresses a problem where the evaluator
was no longer marked as an evaluator on the
client but was still the evaluator for the
server.

This would happen when running the code sample
from #10176 in a separate process.
2020-07-12 11:49:05 +02:00
José Valim 8b5d6bdd4f Do not automatically accept multiple pry requests from same process, closes #10176 2020-07-12 10:43:31 +02:00
José Valim 4d44dac703 Also add :iex by default to application tracers 2020-07-12 09:45:05 +02:00
Justin Smestad 12001c6c28 Fix minor typo in CHANGELOG (#10178)
"Foy" -> "For"
2020-07-12 09:21:44 +02:00
José Valim 3dd567c362 CHANGELOG for config/runtime.exs
Closes #9884
2020-07-11 19:08:01 +02:00
José Valim b3d6aff7b6 Load config/runtime.exs in escripts 2020-07-11 18:30:46 +02:00
José Valim 7aa93b188c Improve compiler error message when using | in a def pattern 2020-07-11 10:22:28 +02:00
Eric Meadows-Jönsson a7e1e8b20f Remove tracers before prying (#10177)
Fixes the following error inside pry:

    ** (ArgumentError) argument error
    (stdlib 3.13) :ets.member(Mix.Compilers.ApplicationTracer, IEx.Helpers)
        (mix 1.11.0-dev) lib/mix/compilers/application_tracer.ex:31: Mix.Compilers.ApplicationTracer.trace/2
        (elixir 1.11.0-dev) src/elixir_env.erl:36: :elixir_env."-trace/2-lc$^0/1-0-"/3
        (elixir 1.11.0-dev) src/elixir_env.erl:36: :elixir_env.trace/2
        (elixir 1.11.0-dev) src/elixir_dispatch.erl:161: :elixir_dispatch.do_expand_import/6
        (elixir 1.11.0-dev) src/elixir_dispatch.erl:91: :elixir_dispatch.dispatch_import/5
2020-07-10 21:12:23 +02:00
José Valim e427599eaf Make sure app loading happens inside recursive task, closes #10175 2020-07-10 18:18:23 +02:00
José Valim dc3a261761 Update CHANGELOG 2020-07-10 16:09:57 +02:00
José Valim dce3f6eb6e Add app.config 2020-07-10 16:09:57 +02:00
Daniele Megna 2885c01da8 Add missing subtract client function in calculator example (#10173) 2020-07-10 09:27:11 +02:00
Wojtek Mach ba0b09f0fb Add Date.beginning_of_month/1 and Date.end_of_month/1 (#10171) 2020-07-10 08:57:14 +02:00
José Valim cf77f5f5da Fix race condition in case of dead captures on io devices
There is a chance a process with input capture has died
but its DOWN message has not been processed yet. So we
check if all process are alive when enabling the new
capture and release them if they are not.
2020-07-09 10:24:02 +02:00
Pablo Costas cf36a9aaef Change erl binary chmod in release to 755 (#10168)
Closes #10168
2020-07-09 09:58:09 +02:00
Gregory Ostermayr 9cbd6dcda7 Redact Git username/password in Mix output log (#10162) 2020-07-08 21:39:51 +02:00
José Valim faec14746f Add config_target() 2020-07-08 17:06:10 +02:00
Eric Meadows-Jönsson a122c512a9 Optimize variable unification (#10166)
Avoids building large chains of variable references by always resolving
to the last variable.

Instead of building a chain of references:

    var1 -> var2 -> ... -> varN -> type()

We now create a direction reference:

    var1 -> varN -> type()

This change vastly improves the speed of checking functions with high
complexity and many variables.
2020-07-08 15:02:46 +02:00
John Bampton de2010649d Remove trailing whitespace (#10164) 2020-07-08 09:01:02 +02:00
John Bampton 4d2c491e19 Standardize README heading (#10163) 2020-07-08 09:00:41 +02:00
José Valim 67c30abb4a Reduce matrix on Windows
Unfortunately builds on Windows are very unstable,
so we simplify the matrix to test a single Erlang
version.
2020-07-08 08:57:22 +02:00
Po Chen 85968b6009 Update CHANGELOG.md (#10160) 2020-07-07 09:21:40 +02:00
José Valim 3b9d262dec Update CHANGELOG 2020-07-06 21:17:42 +02:00
José Valim b2270699e1 Add config_env() and allow imports to be disabled in config files
Also used new functionality throughout Mix, including releases,
which will now prefer to load a config/runtime.exs file over
config/releases.exs.
2020-07-06 21:09:02 +02:00
Eric Meadows-Jönsson b41c3fc65d Use remote default branch instead of master (#10157) 2020-07-06 17:52:27 +02:00
Eric Meadows-Jönsson 801f52fc5d is_map_key/2 should add optional keys (#10156)
Closes #10148.
2020-07-06 17:14:30 +02:00
Thomas Citharel 9ff0985599 Fix a typo in CHANGELOG.md (#10155) 2020-07-06 15:03:17 +02:00
José Valim dccbbf3731 Only show rev if one is given 2020-07-06 13:39:48 +02:00
Pedro Assunção b356a2f20d Show ref information when getting or updating mix dependencies from git sources (#10147) 2020-07-06 13:06:33 +02:00
José Valim 5aabde9b8e Clarify multiline support, closes #10152 2020-07-04 21:11:59 +02:00
John Bampton 89016215e7 Lint YAML for trailing spaces and indents (#10151) 2020-07-04 19:44:51 +02:00
John Bampton 5e39868303 Remove trailing spaces from Markdown (#10150) 2020-07-04 19:12:02 +03:00
José Valim 284881c370 Undeprecate Keyword.replace/3 and Map.replace/3
Previously they were removed because they were seen
as alternatives to `update`, but it makes more sense
to see them as alternatives to `put`.
2020-07-04 13:06:02 +02:00
John Bampton 9699ec1c0e Remove extra blank lines from Markdown in some docs (#10149) 2020-07-04 09:10:03 +03:00
Eksperimental b792ae50c3 Display file:line in already defined type error message (#10145) 2020-07-03 08:32:51 +02:00
Eksperimental 11c3c663bc Correct mix format error messages (#10146) 2020-07-03 08:19:34 +02:00
José Valim d8785b146c Sync port specs with Erlang, closes #10143 2020-07-02 17:21:24 +02:00
Denys Klymenko 402c3c51e6 Fix typo on macro docs (#10144) 2020-07-02 16:35:48 +02:00
Eric Meadows-Jönsson 1b7c4c3944 Improve map warnings (#10142) 2020-07-02 16:14:01 +02:00
José Valim 9a91cd5cb0 Add app.load task
Prior to this commit, application loading was scattered
around Mix, which lead to inconsistent behaviour.

We now always load the applications before compiling,
simplifying the logic on both app.start and in the
application tracer. This also paves the road to
config/runtime.exs, which needs all apps loaded
before they are started.
2020-07-02 15:17:48 +02:00
José Valim e2de345c26 Improve docs on map field access 2020-07-02 13:25:49 +02:00
José Valim f118e58254 Run the formatter 2020-07-02 11:15:11 +02:00
José Valim 99622c6ab3 Support optional applications to be declared
They will most often be declared as:

    [extra_applications: [:foo, :bar, bat: :optional]]

Right now, optional applications have no use. In the future,
they will be used to declare :ex_unit and :mix as optional
dependencies, so we get more precise warnings.

Future OTP versions may also support optional_applications in
the generated .app file.

Closes #10137.
2020-07-02 11:07:27 +02:00
José Valim 2ccef8c503 Avoid more escaping on Windows 2020-07-01 15:51:48 +02:00
José Valim 089d0cd6b6 Use macros instead of special casing in the compiler 2020-07-01 15:49:14 +02:00
José Valim 8895603c21 Improve coverage for types that look like built-in types, closes #10140 2020-07-01 15:18:49 +02:00
José Valim a948b42e0e Clean up typespec suite 2020-07-01 15:08:18 +02:00
José Valim b4040b99d7 Remove quotes to address escaping on Windows 2020-07-01 13:03:40 +02:00
José Valim 522af0821b Remove spaces from tmp_dir and use it in more places 2020-07-01 11:47:09 +02:00
José Valim 76a59a7ab4 Another attempt at removing races 2020-07-01 09:12:22 +02:00
Po Chen f6cd75b978 Update CHANGELOG.md (#10141) 2020-07-01 08:23:16 +02:00
José Valim 1ccfdcfe6b Fix mtime race condition in tests 2020-07-01 00:28:19 +02:00
José Valim 4a2047af1f Keep proper precedence in guards generated by in/2, closes #10138 2020-07-01 00:17:16 +02:00
José Valim 739a41f54f Do not use backticks in error messages, simplify field/fun printing (#10139) 2020-06-30 22:35:22 +02:00
José Valim 0133ccd692 More changelog fixes 2020-06-30 22:15:24 +02:00
José Valim c352e4f562 Update CHANGELOG 2020-06-30 21:45:52 +02:00
José Valim b57852d49f Support --export-coverage in mix test 2020-06-30 16:46:48 +02:00
José Valim cafb4c2ae2 Update CHANGELOG 2020-06-30 16:40:20 +02:00
José Valim 16d3522a6c Update TODOs 2020-06-30 15:51:33 +02:00
José Valim d89c31c056 Introduce rel_templates_path and releases_config_path
This also fixes a bug where rel/overlays directories
would only be included in some occasions. The :overlays
option has therefore been generalized to support
multiple entries.
2020-06-30 15:51:33 +02:00
José Valim bcdeeb3e96 Document loadpaths 2020-06-30 15:51:33 +02:00
Eric Meadows-Jönsson ea885f293c Add map checks (#10136) 2020-06-30 13:26:52 +02:00
José Valim a00178092e Raise if lists have more than 1023 elements in guards
Closes #10114
2020-06-29 15:13:28 +02:00
Wojtek Mach e935cf0784 Add :tmp_dir tag to ExUnit (#10135) 2020-06-29 13:51:07 +02:00
Wojtek Mach 6db8897230 Properly handle --repo and --organization on *.install tasks (#10134) 2020-06-29 09:12:19 +02:00
José Valim c091f9bab7 Fix xref format across multiple OSes 2020-06-28 14:34:14 +02:00
José Valim 72adfa330d Normalize paths before expanding them (#10132)
Closes #9896
2020-06-28 14:29:50 +02:00
Fernando Tapia Rico fb4474de0a Add GitHub Actions CI badge to README (#10130) 2020-06-27 10:52:54 +02:00
Fernando Tapia Rico b5266b0fd0 Use GitHub Actions as CI for Linux (#10129) 2020-06-26 17:26:51 +02:00
José Valim 46bd011fef More CHANGELOG 2020-06-26 16:08:58 +02:00
José Valim 2d9a64f147 Add mix xref graph --format cycles 2020-06-26 15:32:54 +02:00
José Valim ddf1e1452c Clean up and add docs to new beam stripping behaviour 2020-06-26 14:20:13 +02:00
Pedro Assunção 6db550fd3a Adding support for being more granular when stripping beam files (#10125) 2020-06-26 14:01:45 +02:00
Eric Meadows-Jönsson e6e5d2a98d Add Version test for pre-release fields
If a pre-release field compares lexically or numerically higher then it
does not matter that it may have less fields in total.

Closes #10126.
2020-06-26 12:56:01 +02:00
José Valim 89cec027ba Include transitive dependencies in mix xref graph
Previously, the --label option would restrict to
only direct dependencies. But now it shows transitive
dependencies. This means that now, using --sink in
combination with --label compile will directly show
all files that will be recompiled if the given sink
changes.

For example, in Hex.pm we now get:

    $ mix xref graph --sink lib/hexpm/accounts/user.ex --label compile
    lib/hexpm/billing/hexpm.ex
    └── lib/hexpm/billing/billing.ex (compile)
    lib/hexpm/billing/local.ex
    └── lib/hexpm/billing/billing.ex (compile)
    lib/hexpm/emails/bamboo.ex
    ├── lib/hexpm/accounts/email.ex (compile)
    └── lib/hexpm/accounts/user.ex (compile)
    lib/hexpm/emails/emails.ex
    └── lib/hexpm_web/views/email_view.ex (compile)
    lib/hexpm_web/controllers/api/docs_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/key_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/organization_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/organization_user_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/owner_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/package_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/release_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/repository_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/api/retirement_controller.ex
    └── lib/hexpm_web/controllers/auth_helpers.ex (compile)
    lib/hexpm_web/controllers/blog_controller.ex
    └── lib/hexpm_web/views/blog_view.ex (compile)
    lib/hexpm_web/endpoint.ex
    ├── lib/hexpm_web/plug_parser.ex (compile)
    └── lib/hexpm_web/session.ex (compile)

It is now easy to see that, even though `auth_helpers.ex` does
not depend on `user.ex`, it is a common compile-time dependency
that forces Elixir to recompile whenever the user changes.

We have also improved the tree printing algorithm to prefer
flat trees rather than deep ones. This improves the output of
`app.tree` and `deps.tree` as well.
2020-06-26 12:40:40 +02:00
José Valim 319ba7157e Update CHANGELOG 2020-06-25 20:54:14 +02:00
Fernando Tapia Rico 51755e2ee9 Use monotonic times to measure compilation times
By using monotonic times the accuracy of the elapsed time is not
affected by adjustments in the system time.
2020-06-25 20:37:00 +02:00
Michał Muskała aace60b863 Fix bug in Inspect.AlgebraTest (#10128) 2020-06-25 17:03:32 +02:00
José Valim 0ef18f9892 Fix the build 2020-06-25 16:41:18 +02:00
José Valim 74b33b752a Fix the build 2020-06-25 15:59:28 +02:00
José Valim 0166ee82a1 Do not read from ExCk chunk if not necessary
Since all information is in the debug chunk, we use
that instead. This also allows us to remove
`no_warn_undefined` from the ExCk chunk.
2020-06-25 15:34:46 +02:00
José Valim e51cc8b88c Also track exports API via local deps 2020-06-25 15:09:14 +02:00
José Valim 16fafe2117 Fix CI 2020-06-25 13:01:57 +02:00
José Valim 8b4ec95a99 Revert "Consistently handle empty blocks"
Actually, we don't allow `-> [EMPTY]` anywhere else.

This reverts commit 3c6891cb7d.
2020-06-25 12:32:07 +02:00
José Valim 2b2587c68c Include compilation time of individual modules on --profile=time
For example, in Plug, you might see:

    [profile] lib/plug/conn.ex compiled in 935ms
    [profile] lib/plug/ssl.ex compiled in 147ms (plus 744ms waiting)
    [profile] lib/plug/static.ex compiled in 238ms (plus 654ms waiting)
    [profile] lib/plug/csrf_protection.ex compiled in 237ms (plus 790ms waiting)
    [profile] lib/plug/debugger.ex compiled in 719ms (plus 947ms waiting)
    [profile] Finished compilation cycle of 60 modules in 1802ms
    [profile] Finished group pass check of 60 modules in 75ms

This commit also increases the precision of --long-compilation-threshold
which now tracks only the time spent compiling and not the time waiting.
For this reason, we have reduced the threshold time to 10 seconds.
2020-06-25 12:29:59 +02:00
José Valim 3c6891cb7d Consistently handle empty blocks 2020-06-24 20:48:05 +02:00
José Valim 9a6db666a1 Do not make requires/imports compile-time dependencies
Previously, imports and requires were always added as
compile-time dependencies. With this change, they are
"export" dependencies, which are dependencies that depend
on the API defined by a module, such as struct and its
public definitions.

This change should drastically remove the amount of
modules recompiled in Elixir, making imports more feasible
for large projects.
2020-06-24 14:30:00 +02:00
Wojtek Mach 71f690543a Add --repo to archive.install and escript.install (#10121) 2020-06-22 17:12:18 +02:00
José Valim 37f11279e3 Sort the output of mix deps alphabetically 2020-06-22 11:55:00 +02:00
Qqwy / Wiebe-Marten 3ed1a66432 Fixes tiny typo (missing article) (#10119) 2020-06-22 01:01:36 +02:00
Ioannis Pinakoulakis 6666b7bb31 Typos on the erlang files (#10118) 2020-06-21 11:14:11 +02:00
Ioannis Pinakoulakis 2b264cbd32 Fixes typos in comments and documentation (#10117) 2020-06-20 15:19:57 +02:00
Ioannis Pinakoulakis bdaf42bf15 Fix typo in Logger documentation (#10116) 2020-06-20 11:12:35 +02:00
Pavel Shpak 6a8867c66f Fix typo in Typespecs page (#10113) 2020-06-20 10:13:12 +02:00
José Valim 69df6543fb Remove reference to Erlang compile notes 2020-06-20 10:12:01 +02:00
Eksperimental 92756bd679 Correct doc, add spec and update TODO comment in System.stacktrace/0 (#10112) 2020-06-20 09:43:02 +02:00
Mitchell Henke 75e5609364 include callers in translated Logger metadata for Task (#10111) 2020-06-19 20:50:48 +02:00
Eksperimental 83ddb923d4 Mention the word "file" when refereing to .fomatter.exs (#10109) 2020-06-18 17:14:35 +02:00
Eksperimental f1151b27c5 Avoid first person, and improve wording (#10108) 2020-06-18 17:13:35 +02:00
José Valim 5725832003 Clarify whereis docs, closes #10106 2020-06-17 16:11:34 +02:00
Eksperimental 53d66875d7 Use Erlang/OTP "VERSION", instead of OTP "VERSION" (#10105) 2020-06-17 15:13:17 +02:00
Andrea Leopardi 18540250e2 Document performance updates to --/2 since OTP 22
Closes #10104.
2020-06-17 14:53:31 +02:00
José Valim 8744a3cde8 Clarify use cases around at_exit/1 2020-06-16 19:58:23 +02:00
José Valim 4bc45a7b63 Make exception types public, closes #10103 2020-06-16 11:05:37 +02:00
José Valim b56c262db3 Only run in interactive mode if there is a config provider
We also don't hardcode the RELEASE_MODE config variable
inside Config.Provider, instead we use the application
environment. To make this possible, we simplify the boot
signature for the provider so it only calls boot/0 instead
of a dynamic app-key pair.
2020-06-14 10:53:26 +02:00
Samuel Gordalina ed63846b4d Change release default mode to interactive (#10091) 2020-06-14 09:14:32 +02:00
Kelvin Stinghen 83374a88c9 Rewording the default arguments warnings (#10097) 2020-06-13 11:28:09 +02:00
José Valim 3d1c1b4e23 Use global gitconfig instead of individual options on commands
Closes #10098.
2020-06-13 10:15:53 +02:00
José Valim cc138f37f2 Add line numbers to deprecated imports 2020-06-11 15:59:07 +02:00
José Valim 0df37749af Use reserved word where appropriate instead of token/keyword 2020-06-11 10:34:50 +02:00
José Valim 8148d079bf Remove code duplication between calendar modules 2020-06-11 09:51:37 +02:00
Kelvin Stinghen 091bb1b5b5 Adding gregorian seconds functions to DateTime (#10089) 2020-06-11 09:35:17 +02:00
Kelvin Stinghen b76e73abbc Returning microseconds on NaiveDateTime.to_gregorian_seconds (#10082) 2020-06-11 09:31:48 +02:00
Wojtek Mach 3757e36ed7 Don't add compile-time dependency on defdelegate (#10093)
The dependency doesn't seem needed in this case.

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

An easy way to see this optimization is:

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

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

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

vs:

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

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

    $ touch lib/b.ex && mix compile --verbose
    Compiling 2 files (.ex)
    Compiled lib/b.ex
    Compiled lib/a.ex
2020-06-11 09:31:12 +02:00
José Valim 9c99bc28a3 Unify guards in __do_log__ 2020-06-10 01:38:57 +02:00
Wojtek Mach d2cb4f4063 Run formatter (#10090) 2020-06-09 22:49:27 +02:00
José Valim 5257bbe548 Make sure Logger.log/bare_log does not crash with :warn 2020-06-09 21:56:48 +02:00
José Valim 5cee48fdc8 Use new logger_enabled metadata 2020-06-09 20:17:48 +02:00
James Russo ef2785326c Add new! to Calendar Types (#10069)
Add new! to Date/Time/NaiveDateTime/DateTime. This also adds
new to DateTime.
2020-06-07 19:34:53 +02:00
José Valim 0bbcb8f280 Add is_map_key as a function that returns booleans 2020-06-07 17:27:12 +02:00
Wojtek Mach cfba0c8f34 Print which deps got unlocked (#10085)
Before this patch, we only printed which deps got unlocked when
`--filter` option was used. Now we do it everywhere else, that is
when passing specific apps to unlock and when using `--unused`.
2020-06-06 11:04:03 +02:00
José Valim 779ccdb756 Do not diff guards if pattern does not match 2020-06-04 20:15:25 +02:00
Garth Kidd d9e01a61c9 Clarify @doc for Registry.unregister_match/4 (#10079) 2020-06-04 09:01:51 +02:00
Peck 5d6da17e29 Replace whitelist/blacklist terminology with allowlist/denylist (#10078) 2020-06-02 19:59:28 +02:00
José Valim 76c80e9552 Do not raise on foreign types, closes #10077 2020-06-02 15:06:21 +02:00
sampriddy 58483e0445 Undo change removing assigns from Mix.Generator.copy_template docs (#10075) 2020-06-01 19:35:52 +02:00
sampriddy 1a1dc609c0 Corrections to docs Mix.Generator docs (#10074) 2020-06-01 19:18:58 +02:00
Jeroen Visser bdd2218b61 Link to Typespec docs from Module documentation (#10072) 2020-06-01 16:43:11 +02:00
Qqwy / Wiebe-Marten b9209ad1ac Improves 'patterns-and-guards' documentation page (#10065) 2020-05-28 18:59:28 +02:00
IKEDA Hiroki d155966074 Doctest to generate tests also from @typedoc (#10059) 2020-05-25 19:38:18 +02:00
JoseHZBY 7456b5789b Update patterns-and-guards.md (#10064) 2020-05-25 18:37:50 +02:00
Waldemar Rachwał 7c8ca5a3af Fix read of app file located in archive .ez file (#10062) 2020-05-25 18:32:50 +02:00
Cẩm Huỳnh 291ebf7458 Check function arity in ParallelCompiler.async (#10060) 2020-05-24 18:09:14 +02:00
Cleidiano Oliveira 70da709881 Allow user to define a type named union (#10055) 2020-05-23 12:41:17 +02:00
Justin Schneck 06e8bfacc0 Update docs for Mix.target to remove experimental (#10056) 2020-05-22 20:34:09 +02:00
José Valim 10fdb8e2d4 Provide exception blaming to linked and trapped exits in ExUnit 2020-05-22 10:58:12 +02:00
José Valim 44061c8499 Do not crash when checking empty receive 2020-05-22 10:58:12 +02:00
Alexander Koutmos 50995e6640 Added @impl to application.ex generated by mix new (#10052) 2020-05-19 20:16:20 +02:00
Michał Łępicki ce05be75eb Correct the specs for Enum aggregate functions (#10050) 2020-05-19 12:25:45 +02:00
Michał Łępicki df58c00e20 Update Enum.min_max_by/3 spec after #10033 (#10049)
#10033 added a third argument sorter_or_empty_fallback with a default value. To not break compatibility with existing code it also added a function clause that checks third argument's type with a guard and calls Enum.min_max_by/4 with appropriate arguments.

This commit fixes the following dialyzer warnings:

- In user code, e.g. `Enum.min_max_by([1, 2, 3], fn x -> x end, fn -> raise(Enum.EmptyError) end)`:
    The function call will not succeed.
    Enum.min_max_by([1, 2, 3], (_ -> any()), (() -> none()))
    will never return since it differs in arguments with
    positions 3rd from the success typing arguments:
    (any(), (_ -> any()), atom() | (_, _ -> boolean()))

- When running dialyzer on Elixir itself (erlang notation):
    Invalid type specification for function 'Elixir.Enum':min_max_by/3. The success typing is 
    (_, fun((_) -> any()), atom() | fun((_, _) -> boolean())) -> any()
2020-05-19 11:20:41 +02:00
Daniel Fischer 61910595d3 Fix typo (#10048)
Update "choosen" to "chosen"
2020-05-19 07:52:18 +02:00
Kip Cole 1f80700c10 Add :nfkd and :nfkc forms to String.normalize/2 (#10046) 2020-05-18 21:08:31 +02:00
José Valim fa76ac65db Add extra suggestions when Hex can't be installed, closes #10039 2020-05-18 10:05:03 +02:00
José Valim b028ac88a8 Improve references around functions 2020-05-17 16:05:45 +02:00
Wojtek Mach c592d1300d Update docs (#10043) 2020-05-17 12:52:30 +02:00
Dallin Osmun 479b2890ce Report the correct line number when raising inside a macro (#10040) 2020-05-16 09:32:22 +02:00
Michał Łępicki 608a023fa7 Pass a valid os option to filename:basedir/3 (#10041)
unix is not recognized and gets interpreted as linux
2020-05-16 09:29:41 +02:00
Wojtek Mach 42af53f094 Add ExUnit.fetch_test_supervisor/0 (#10035) 2020-05-15 19:40:55 +02:00
José Valim 1d8521d148 Ensure tmpdir exists before changing directory to it 2020-05-15 19:34:38 +02:00
José Valim ebb0e84897 Add missing CHANGELOG entry 2020-05-15 18:44:01 +02:00
Wojtek Mach 5c05d1ab22 Add CHANGELOG.md to docs (#10038) 2020-05-15 14:18:26 +02:00
José Valim f44a7fabd9 Update CHANGELOG 2020-05-15 10:49:26 +02:00
Andrew France 1b1d365443 Document data and fun arguments to update_in/3 (#10034) 2020-05-14 12:27:23 +02:00
Jeroen Visser c12a130e7c Allow a sorting function on Enum.min_max_by/3,4 (#10033)
This should keep backwards compatibility as it relies on the arity of
the sorter to determine if it is a empty_fallback or a sorter.
2020-05-14 12:26:59 +02:00
José Valim 533f2f7d2f Stacktrace may no longer be available 2020-05-14 01:53:04 +02:00
José Valim f89d809d0b Add coloring support for Windows (#10031)
Closes #9810.
2020-05-14 01:05:07 +02:00
Bryan Paxton fc9f7690c1 Add support for new rebar3 output env option (#10018) 2020-05-14 01:04:19 +02:00
José Valim c89b16a98a Introduce MIX_XDG (#10028) 2020-05-14 01:03:20 +02:00
José Valim 68aa544d90 v1.10.3 is compatible with Erlang/OTP 23 2020-05-13 12:55:46 +02:00
Wojtek Mach 1f6e7093cf Add :cd option to Mix.Shell.cmd/2 and --cd to mix cmd (#10029) 2020-05-12 15:43:09 +02:00
José Valim f837246027 Skip test on Windows 2020-05-11 09:38:02 +02:00
Ryan Zidago 181c38f16e Fix Task PID and caller in Task Supervisor reports (#10025)
Closes #10024
2020-05-09 01:14:09 +02:00
Michał Muskała 655ce6d3b4 Add note that System.stacktrace/0 will always return an empty list (#10023)
Starting with OTP 23 erlang:get_stacktrace/0 will always return an
empty list. The function will be completely removed in OTP 24 - we
should plan appropriately for that.
2020-05-08 16:22:24 +02:00
Denis Oblogin 7f174f1fff Add linear representation functions to Calendar types (#10009) 2020-05-07 18:52:32 +02:00
Evgeny Golyshev fe77f54b5f Test not only rebar3 (#10022)
'gets and compiles dependencies for Rebar' uses rebar3 which is wrong.
2020-05-07 13:00:11 +02:00
Anderson Cook c040270bd7 improve performance of Enum.with_index/2 (#10020) 2020-05-06 23:08:51 +02:00
tarreeandSamuel Smith 9235f8e72a Adjust @type body for Registry to match erlang ets match_spec Result (#10017)
Co-authored-by: Samuel Smith <samuel.smith@se.com>
2020-05-05 21:07:37 +02:00
Dave Cottlehuber 285097b41f Enable overriding sys.config location via env var (#9974) 2020-05-05 12:48:10 +02:00
José Valim 6a0973b091 Slightly refactor Mix.Release path handling 2020-05-04 11:31:49 +02:00
Guilherme Pasqualino f4539c3fa3 Load app from dependencies path when it is a project dependency (#10007) 2020-05-04 11:21:20 +02:00
Ryan Zidago 99c587fff5 Fix typo in Registry module documentation (#10015) 2020-05-03 14:09:45 +02:00
José Valim f1b0da335e Run the formatter 2020-05-03 11:20:23 +02:00
Lau Taarnskov 65ea09dcb8 Improve documentation for utc_offset and std_offset fields (#10014) 2020-05-03 09:42:18 +02:00
Johan van der Vleuten f86d8197fe Improve Mix.Config docs to point to the Elixir library for Config and Config.Reader (#10013) 2020-05-02 16:00:17 +02:00
José Valim f3c4aae804 Improve tests for cond 2020-05-02 14:01:52 +02:00
José Valim 2dacb59d36 Support proper encoding in releases config (#10012) 2020-05-01 20:13:50 +02:00
José Valim 3fd68ef62a Enforce keys to be atoms in Keyword.keys/1, closes #10010 2020-05-01 15:48:03 +02:00
José Valim bdde9e619b Revert "Add String.titlecase/2 (#10008)"
Unicode says "HH" is not titlecased, which means we need
to downcase the rest of the string, which is the behaviour
we already have in capitalize.

This reverts commit 3be3afb920.
2020-04-30 21:34:25 +02:00
José Valim 1fd15774a4 Run the formatter 2020-04-30 21:18:18 +02:00
José Valim 57181bfc77 Improvements to titlecase 2020-04-30 21:18:05 +02:00
Denis Oblogin 3be3afb920 Add String.titlecase/2 (#10008) 2020-04-30 21:11:02 +02:00
José Valim 199acd1c2f Update CI and use taskset 1 for reproducible builds (#10005) 2020-04-30 12:49:09 +02:00
Wojtek Mach 276a8bf82d Add a note about @external_resource not being there yet (#10006) 2020-04-30 12:25:52 +02:00
José Valim 4fea9cc942 Recompile Kernel before bootstrapping stdlib (#10002)
Before this patch, we could have slightly different
AST while compiling modules in parallel, depending
if modules were recompiled or not. Now we always
recompile Kernel before compiling the whole stdlib.

We used this opportunity to simplify bootstrap by
breaking it two steps. The first step is the minimal
necessary to compile Kernel itself. The second step
contains the whole compiler.

Closes #10000.
2020-04-30 01:38:04 +02:00
José Valim 381bd68624 Improvements to the diff script which helped tackle #10000 2020-04-30 00:52:59 +02:00
José Valim 8bcbfe9a87 Do not send keyword through static_atoms_encoder, closes #9996 2020-04-29 11:09:05 +02:00
Michał Łępicki 86534cf7ec Fix spec for Mix.ProjectStack.compile_env/1 (#9994)
This function is getting called with :unset in Mix.Tasks.Compile.App.load_compile_env/1
2020-04-28 23:09:02 +02:00
Michał Łępicki 295ebf4018 Fix handling of empty user home flag in System.user_home/0 (#9991)
According to erlang documentation, :init.get_argument/1 returns :error when no value is associated with the flag: http://erlang.org/doc/man/init.html
2020-04-28 13:52:17 +02:00
Michał Łępicki d015598d71 Remove redundant clause in ExUnit.Formatter.format_sides/6 (#9988)
The formatter argument is a function so never should be equal to :expr
2020-04-28 13:50:43 +02:00
Michał Łępicki 80646cf56e Call :zip.extract/2 with cwd option as charlist (#9989)
This function call is working when cwd is a binary right now, but it is documented that cwd should be a charlist
2020-04-28 13:50:12 +02:00
Michał Łępicki ad8a69dee0 Remove error handling of empty escript name in Mix.Tasks.Escript.Build.escriptize/2 (#9990)
When no name was provided, project :app name is being used
2020-04-28 13:49:42 +02:00
Michał Łępicki c9e1f85572 Fix spec for Mix.Local.Installer.parse_args/2 (#9992)
The function can also return an {:error, message} tuple when 
parsing is not successful.
2020-04-28 11:13:48 +02:00
Vinicius Ferreira Negrisolo e72d4d1cdc Document Task.async_stream/5 infinity timeout (#9984)
Task.async_stream/5 accepts `timeout: :infinity` option.
2020-04-28 00:05:29 +02:00
Michał Łępicki abb6e10a10 Fix spec for Mix.Project.deps_apps/0 (#9985) 2020-04-28 00:04:59 +02:00
Michał Łępicki 986bd30072 Update Mix.Release.t type specification (#9983) 2020-04-27 20:34:59 +02:00
Michał Łępicki 14be5e4473 Call Mix.Dep.Lock.write/1 with a map in Mix.Tasks.Deps.Unlock.run/1 (#9982) 2020-04-27 19:38:25 +02:00
Michał Łępicki e8cb3489c9 Fix spec for Exception.blame_mfa/3 (#9981) 2020-04-27 19:14:39 +02:00
Michał Łępicki e7139df2bf Remove redundant pad handling in Calendar.parse_modifiers (#9977)
The - padding is already handled at line 526
2020-04-26 20:49:17 +02:00
Michał Łępicki a726fcb546 Inline types not exported from their erlang modules (#9976) 2020-04-26 18:19:36 +02:00
Michał Łępicki b6f0604880 Include all relevant OTP apps when building dialyzer PLT (#9975)
Fix many "Unknown function" and "Unknown type" dialyzer errors
visible when generating the dialyzer PLT and when running dialyzer analysis.
2020-04-26 18:06:34 +02:00
indi f2a4b55fdb Fix a typo in the Application docs (#9971) 2020-04-26 12:55:33 +02:00
José Valim 8770a61769 Handle ; inside empty parens, closes #9964 2020-04-26 11:42:20 +02:00
Arthur Bailão cd88b69dbe Fix deep nested lists inside docs (#9970) 2020-04-26 11:13:04 +02:00
José Valim 9dc480d3ba Do not use lists:concat, closes #9973 2020-04-26 11:10:13 +02:00
José Valim 3fe3accc05 Force modules to be loaded when they are required 2020-04-26 11:03:30 +02:00
Michał Łępicki 0eb3321c10 Update type spec for IEx.color to indicate that it works with iodata (#9967)
This fixes a few dialyzer errors, e.g. in IEx.Introspection.open/1
or in IEx.Helpers.print_pane/1
2020-04-26 09:43:45 +02:00
Michał Łępicki 2b8c2210b0 Fix type specifications for EEx.Tokenizer.tokenize/4 (#9966) 2020-04-26 09:42:54 +02:00
Michał Łępicki 3825c79dda Fix return type for ExUnit.OnExitHandler.register/1 (#9968) 2020-04-26 09:42:23 +02:00
Michał Łępicki 4da2b27863 Fix multiple dialyzer errors in the Elixir application (#9965)
* fix dialyzer issue in ParallelChecker.verify/2

* fix dialyzer specs for elixir_errors:form_error and elixir_errors:form_warn

* fix dialyzer error caused by erl_anno:anno type being opaque in exception.ex

* fix spec for File.stream!

* fix spec for IO.getn/2 - dialyzer doesn't support overloaded specs

* add explicit spec for System.halt/0 to fix dialyzer error

* fix spec for Regex.recompile/1
2020-04-26 09:40:40 +02:00
Yiming Chen 14f843bc8b Fix mix cmd examples in its moduledoc (#9969)
- `mix cmd echo pwd` would only print literally `"pwd"` for each child app
- `mix cmd pwd` would print each child app's path
2020-04-26 09:24:07 +02:00
José Valim ec13373031 Allocate line zero ann just once 2020-04-25 20:41:53 +02:00
Michał Łępicki 7094e9754c Fix dialyzer errors caused by erl_anno:anno type being opaque (#9961) 2020-04-25 20:29:56 +02:00
Mikael Muszynski 7c19db6727 Fix erroneous code snippet in documentation (#9963)
The return value in the code example is the opposite of what the code
would actually return, making the example misleading.
2020-04-25 18:12:20 +02:00
Wojtek Mach bdb5072925 Update EEx docs (#9962) 2020-04-25 17:55:31 +02:00
José Valim f1704d42bc Update CHANGELOG 2020-04-25 10:47:06 +02:00
José Valim 8ec9860a4e Ensure generated docs can be overridden without explicit clause 2020-04-25 00:35:04 +02:00
Alexei Sholik 2c8fe38a07 Improvements in Time.diff() docs (#9955) 2020-04-25 00:06:18 +02:00
Cohen Carlisle 2627fa49c5 Update Enum.join and map_join docs (#9960)
* reference string instead of binary for both functions
* remove outdated reference to list joiner in map_join
2020-04-25 00:05:51 +02:00
José Valim c3dc9ddd57 Run the formatter 2020-04-24 18:56:04 +02:00
José Valim ac9dec8daa Respect :path when tar-ing releases, closes #9949 2020-04-24 18:11:15 +02:00
José Valim 775eed1567 Always validate given IEx options, closes #9959 2020-04-24 00:11:42 +02:00
José Valim 5ea927e081 Fix diffing for variable structs, closes #9958 2020-04-23 21:39:48 +02:00
José Valim 7be4c5051c Fix build on Windows 2020-04-23 21:01:28 +02:00
José Valim 1387035845 Do not assume all files have started to compile, closes #9943 2020-04-23 20:07:42 +02:00
José Valim 89f733e258 Do not attempt to load in memory beam files, closes #9788 2020-04-23 15:58:29 +02:00
José Valim 9b5cafe72a Bring back String.normalize and fix infinite loop 2020-04-23 13:35:49 +02:00
José Valim 138c1bd9ed Fixes to ExUnit.Diff, closes #9926 2020-04-21 20:24:09 +02:00
Wojtek Mach b79a97a6b6 Add __mix_recompile__?/0 callback (#9919) 2020-04-21 13:40:47 +02:00
Ivan Rublev 29f29419a2 Add +++ and --- with right associativity to the list of custom operators (#9954) 2020-04-21 13:03:31 +02:00
Wojtek Mach 48ef163566 Update :skip_undefined_reference_warnings_on in docs.exs (#9953) 2020-04-20 09:51:54 +02:00
Sushant Bajracharya 1ce201aa1e Use __MODULE__ alias (#9952) 2020-04-19 10:04:30 +02:00
José Valim 2d20e30263 Reload applications in app manifest for changed local deps 2020-04-18 20:36:50 +02:00
Dan 80c45ed673 Update release.ex documentation (#9950) 2020-04-18 10:22:03 +02:00
Michał Szajbe 62db05ded2 Remove references to module in local_function and local_macro events docs (#9945) 2020-04-16 12:04:20 +02:00
Dave Cottlehuber f30ee1fec4 Update build to a supported FreeBSD version (#9944) 2020-04-16 09:41:19 +02:00
Eric Meadows-Jönsson cb1cfdd991 Document environment variable defaults (#9938) 2020-04-15 11:58:04 +02:00
Łukasz Jan Niemier 20ff5114a1 Make spec of c:run/1 match the documentation (#9936) 2020-04-14 12:51:14 +02:00
José Valim 4e8ff12bea Add |/2 to special forms 2020-04-13 21:22:03 +02:00
Andrea Leopardi e30525bdbb Fix formatting in Application.compile_env!/3 docs 2020-04-12 14:23:02 +02:00
Aaron Renner fc12dce7c3 Document that File.cd/1 affects the BEAM globally (#9928) 2020-04-11 16:52:48 +02:00
IanWizard 10a0fd8144 Fixed typo in naming conventions docs. (#9929)
A typo in naming-conventions.md caused a broken link on hexdocs.pm
2020-04-11 16:20:22 +02:00
Michael Wu 1c157bdc54 Fix typo in Macro docs (#9927) 2020-04-10 10:23:15 +02:00
Pablo Costas 110dfe67aa Fix tokenizer emitting repeated warnings in the REPL (#9924)
Now we only emit tokenizer warnings upon tokenization success.
2020-04-10 00:48:49 +02:00
José Valim 3300016a85 Fix Erlang compiler bug on 21.0 2020-04-10 00:46:37 +02:00
Luca Peppe ee9667848a Fix supervisor docs (#9925) 2020-04-09 16:43:06 +02:00
José Valim 8742096f77 Do not emit warwnings for injected funs 2020-04-07 18:23:04 +02:00
José Valim 3be6098300 Improve docs and treat all missing as hidden 2020-04-06 17:14:06 +02:00
José Valim 3d3e233255 Refactor binary handling 2020-04-05 20:15:50 +02:00
Everett Griffiths c257b13291 Fix various typos and improve docs (#9922) 2020-04-05 17:35:21 +02:00
José Valim 68ad287a58 Improve error message for nested binaries in match without specifiers, closes #9908 2020-04-04 12:35:46 +02:00
Tom Hoenderdos 101a801bdd Add guard to Code.unrequire_files/1 (#9921) 2020-04-03 14:40:59 +02:00
José Valim aa0a9b3f4f Update CHANGELOG 2020-04-03 12:00:54 +02:00
Andrea Leopardi 9306ca9ea4 Improve some documentation in the String module 2020-03-29 10:32:12 +02:00
José Valim 5e3ef21c24 Show Erlang docs directly in the shell (#9843) 2020-03-28 20:43:45 +01:00
Fernando Tapia Rico fd98a3f3cb Update aliases_test.exs 2020-03-28 16:24:54 +01:00
Wojtek Mach 97aef3c5db Deprecate non-atom keys on put_env, get_env, fetch_env*, delete_env (#9915)
Typespecs already required an atom key but it was never enforced by
code. We're doing this because we eventually would like to support
passing a _path_ like:

    Application.get_env(:myapp, [:key, :foo, :bar])

and this way we will be consistent with recently added
`Application.compile_env/3`.
2020-03-28 13:02:23 +01:00
José Valim acf507f821 Remove verbose flag from makefile 2020-03-28 10:51:10 +01:00
José Valim 8491f0dc74 Fix improper regex use, closes #9914 2020-03-28 10:49:23 +01:00
Charlie 6db1b1258e Minor documentation correction (#9913) 2020-03-27 16:18:17 +01:00
Wojtek Mach 2701169b1e Use new ExDoc extras auto-links (#9912) 2020-03-27 00:05:48 +01:00
José Valim 1aea878a7a Add example for runtime_config_path, closes #9911 2020-03-25 10:54:18 +01:00
José Valim f3806f82af Revert "Use an empty keyword list instead of nil for empty definitions"
Closes #9910

This reverts commit 4477115675.
2020-03-24 20:21:01 +01:00
Philip Giuliani d81260ebe7 Fix incorrect spelling (#9909) 2020-03-24 10:49:07 +01:00
José Valim cb916e31cd Run formatter 2020-03-22 20:59:55 +01:00
José Valim 1854bbbe97 Use OptionParser.split/1 2020-03-22 20:48:12 +01:00
João Duarte 35a5a72a2c Adding @requirements to Mix Task (#9905) 2020-03-22 19:47:26 +01:00
José Valim 68c1ae2545 Fix formatting 2020-03-21 19:36:05 +01:00
Glenn Vanderburg 9a9b1f48bc Document operators on the Operators page (#9903) 2020-03-21 08:50:23 +01:00
José Valim 6570d782e4 Pass arguments to the overridden alias
Otherwise overriding certain tasks, such as
"compile", can break the project whenever
"compile" is not the last task in the alias
chain.

Closes #9902.
2020-03-20 22:10:28 +01:00
Wojtek Mach 0d5ca47fda Improve app name detection by the application tracer (#9900)
OTP app directories contain version numbers and they are included in the name:

Before:

    warning: :ssl.versions/0 defined in application :ssl-9.6 is used by the current application but the current application does not directly depend on :ssl-9.6. To fix this, you must do one of:

      1. If :ssl-9.6 is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs
    (...)

After:

    warning: :ssl.versions/0 defined in application :ssl is used by the current application but the current application does not directly depend on :ssl. To fix this, you must do one of:

      1. If :ssl is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs
    (...)
2020-03-19 16:00:55 +01:00
José Valim d54b2ee850 Rollback on mix xref stats change 2020-03-19 13:12:13 +01:00
José Valim a967dc8189 Revert "Move mix xref graph --format stats to mix xref stats"
This reverts commit e69eb8f172.
2020-03-19 13:08:04 +01:00
José Valim 644bc7c5d7 Skip sys_core_prepare pass on Erlang/OTP 23+ 2020-03-18 14:21:13 +01:00
eric 6525df8c8a Improves readability of Keyword module docs (#9897) 2020-03-18 00:27:22 +01:00
Everett Griffiths b2270d7d3d Includes an example of how to use ANSI functions to format output (#9893) 2020-03-17 11:47:19 +01:00
Wojtek Mach ec7622d264 Fix lib/mix/lib/mix/tasks/test.ex markdown (#9894) 2020-03-16 20:10:27 +01:00
Fernando Tapia Rico 72cf0da6fb Improve 'check_reproducible' script (#9879)
The new script performs a "BEAM-diff" in case of BEAM files.
2020-03-15 12:19:20 +01:00
José Valim e3a0f19fff Add missing new line to warning 2020-03-14 15:16:22 +01:00
José Valim 1c03f7990a Update changelog 2020-03-13 11:57:51 +01:00
José Valim d191085117 Compare pins as values, closes #9872 2020-03-13 11:51:40 +01:00
José Valim 1a4efb79a1 Reduce the newlines count when they are immediately followed by a comment, closes #9873 2020-03-13 11:27:06 +01:00
José Valim c36090f3a1 Notify the script has been tested for bash, closes #9890 2020-03-13 09:44:40 +01:00
Hans Bernhard Gödeke 593b3083c4 Warn when @doc attribute is replaced across clauses
Closes #9883
Closes #9888
2020-03-13 09:44:40 +01:00
Eric Meadows-Jönsson cb2167600a Remove list generator clause erlang comprehensions (#9891)
Erlang comprehensions are only used when all generators are binary.
2020-03-12 22:56:27 +01:00
José Valim d375989b28 Do not warn on nullary calls without parens 2020-03-12 13:11:11 +01:00
Boris 862b6ebfee Add default and enforce keys in Version struct (#9886)
String.Chars.to_string/1 was failing due to unexpected `nil` value in `pre` field.
2020-03-09 19:59:23 +01:00
José Valim bc9688f1bb Support aggregate coverage results from umbrellas and OS partitioning (#9880) 2020-03-09 14:47:36 +01:00
Wojtek Mach 7d777ca739 Fix links (#9882) 2020-03-09 10:06:00 +01:00
Cohen Carlisle e19c059a1b Update String.replace/4 and String.split/3 docs (#9876) 2020-03-08 19:09:17 +01:00
Fernando Tapia Rico 94a8c9ad2a Do not update packages on FreeBSD (CI) (#9881)
FreeBSD builds are failing because `pkg` refuses to install 
new packages that were built using more recent OS versions. 

The build does not update the packages to avoid the error.
2020-03-08 14:56:39 +01:00
José Valim a4e909fe1a Refactor summary handling 2020-03-08 11:20:35 +01:00
José Valim 9550632e2c Shutdowns can be any term, closes #9878 2020-03-07 00:20:16 +01:00
José Valim fa9bca45fb Add missing @doc :since 2020-03-07 00:00:46 +01:00
Fernando Tapia Rico ea61a70953 Document version of :start/:stop events (tracers) (#9871) 2020-03-03 20:09:30 +01:00
Eric Meadows-Jönsson 31818a36e6 Do not propagate context between clauses (#9869) 2020-03-03 08:19:20 -08:00
José Valim 6055db482a Add start and stop events to compilation tracers, closes #9868 2020-03-02 14:36:19 +01:00
José Valim 4bd3c9936b Ensure protocols are consolidated after compilation 2020-03-01 16:21:50 +01:00
José Valim 7052f2b31d Remove race condition with cli test 2020-03-01 11:59:03 +01:00
José Valim a80929c209 Unify compilation entry point in the compile Mix task 2020-03-01 11:50:34 +01:00
José Valim c13b5822dd Fix metadata link 2020-03-01 10:40:55 +01:00
Hassan Shaikley c32b5ac837 Documentation wording improved for Supervisor.Spec moduledoc (#9866) 2020-03-01 10:27:32 +01:00
Hassan Shaikley d462795cb8 Documentation wording improved for List.Chars moduledoc (#9865) 2020-03-01 10:26:19 +01:00
Wojtek Mach 561338d5e6 Return [{mod, bin}] from Code.compile_file/2, require_file/2, load_file/2 (#9864)
This was the documented behaviour on 1.9 that was broken on v1.10: (version `git` is this branch)

    $ cat a.ex
    defmodule A, do: nil

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

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

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

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

    [{A, <<70, 79, ...>>}]
2020-03-01 00:50:14 +01:00
José Valim 6963bc9110 Clarify docs around codepoints 2020-02-29 23:46:25 +01:00
Wojtek Mach 3cef9a8bd9 Add guards to Calendar.strftime/3 (#9862)
This improves the error message when we accidentally use the wrong order
of arguments.

Before

    iex(1)> Calendar.strftime("%h", Time.utc_now())
    ** (FunctionClauseError) no function clause matching in Calendar.parse/4

        The following arguments were given to Calendar.parse/4:

            # 1
            ~T[12:26:18.591978]

            # 2
            "%h"

            # 3
            %{
              abbreviated_day_of_week_names: #Function<4.80759775/1 in Calendar.options/1>,
              abbreviated_month_names: #Function<3.80759775/1 in Calendar.options/1>,
              am_pm_names: #Function<0.80759775/1 in Calendar.options/1>,
              day_of_week_names: #Function<2.80759775/1 in Calendar.options/1>,
              month_names: #Function<1.80759775/1 in Calendar.options/1>,
              preferred_date: "%Y-%m-%d",
              preferred_date_invoked: false,
              preferred_datetime: "%Y-%m-%d %H:%M:%S",
              preferred_datetime_invoked: false,
              preferred_time: "%H:%M:%S",
              preferred_time_invoked: false
            }

            # 4
            []

        Attempted function clauses (showing 3 out of 3):

            defp parse("", _datetime, _format_options, acc)
            defp parse(<<"%", rest::binary()>>, datetime, format_options, acc)
            defp parse(<<char, rest::binary()>>, datetime, format_options, acc)

        (elixir 1.11.0-dev) lib/calendar.ex:516: Calendar.parse/4
        (elixir 1.11.0-dev) lib/calendar.ex:507: Calendar.strftime/3

After:

    iex(1)> Calendar.strftime("%h", Time.utc_now())
    ** (FunctionClauseError) no function clause matching in Calendar.strftime/3

        The following arguments were given to Calendar.strftime/3:

            # 1
            "%h"

            # 2
            ~T[12:28:04.638790]

            # 3
            []

        Attempted function clauses (showing 1 out of 1):

            def strftime(date_or_time_or_datetime, string_format, user_options) when is_map(date_or_time_or_datetime) and is_binary(string_format)

        (elixir 1.11.0-dev) lib/calendar.ex:506: Calendar.strftime/3
2020-02-29 13:34:07 +01:00
José Valim a43d0a0656 Remove unused compilation_status code 2020-02-29 11:46:18 +01:00
José Valim 1efda76c7f Remove application level functions as they require Erlang/OTP 22+ 2020-02-29 11:35:08 +01:00
José Valim ead8cf6101 Pass application tracer warnings to the compiler, closes #9861 2020-02-29 10:38:51 +01:00
José Valim 9784707524 Handle nil in :profile accordingly 2020-02-29 10:04:37 +01:00
José Valim cf56af4e05 Update CHANGELOG 2020-02-29 09:45:02 +01:00
José Valim c0a0f2dd70 Use a single option to track profiling data 2020-02-29 09:41:29 +01:00
José Valim 4a75716625 Improve docs and coverege for module levels 2020-02-29 09:20:26 +01:00
Łukasz Jan Niemier 2ce4e761c5 Expose functions for setting per-module levels (#9793) 2020-02-29 09:16:38 +01:00
José Valim 49cd3c13bd Use meta[:line] first and then fallback to env.line 2020-02-28 21:29:08 +01:00
José Valim ea47f8e31f Respect the config path location in umbrella projects 2020-02-28 21:11:40 +01:00
Calvin Buckley 651375ddb2 Fix File module rename tests on PASE (#9860)
IBM i PASE implements rename in a slightly weird way due to it
wrapping the native syscall. Expect this in the File module's
rename test when expecting an error. Fixes #9858.

This should be the behaviour documented (different from
AIX, which it's emulating, and strange for POSIX):

https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_74/apis/renameun.htm
2020-02-28 19:37:16 +01:00
José Valim c23de2c56a Improve logging examples 2020-02-28 16:23:46 +01:00
Łukasz Jan Niemier 78c4192d31 Add structured logging (#9797) 2020-02-28 16:22:31 +01:00
José Valim 9d56a019ee Add application tracers to Mix
This tracer is used to validate that all modules being invoked
in an application belongs to an application that is currently
listed as a dependency. This will help enforce umbrella application
boundaries and help users discover when they depend on Elixir and
Erlang libraries without declaring them upfront.

Thanks to @sasajuric for the proof of the concept and insights
that made this feature possible.

Closes #9847.
2020-02-28 16:05:34 +01:00
José Valim 6bc9b3459e Do not emit duplicate stacktrace warnings 2020-02-28 16:05:34 +01:00
Benjamin Milde 18bf8165be Fix typos in mix release docs (#9857) 2020-02-28 12:43:31 +01:00
Pick a Tag 2e1c4625c0 mix release for Windows 10 has dependency (#9852)
Proposed doc change for issue #9832 
mix release for Windows 10 has dependency towards "Visual C++ Redistributable Packages for Visual Studio 2013"
2020-02-28 09:43:29 +01:00
José Valim e208e1066e Add Mix.Project.deps_apps/0 2020-02-27 21:19:23 +01:00
Everett Griffiths c0ffc15b76 Clarifies explanation of unicode and Regex character classes, includes example (#9845) 2020-02-27 12:02:14 +01:00
Marcus Kruse 339759c8f1 Fix typo in doc for time_zone_periods_from_wall_datetime/2 (#9846) 2020-02-27 09:29:40 +01:00
José Valim 6c9a8495a1 Make EEx trim behaviour more consistent
The previous rules made it hard to understand how it really worked.

The new rules are simpler, as it trims all whitespace, including new
lines, both on left and right sides, leaving at least one newline.
2020-02-26 20:19:28 +01:00
José Valim 351cff9d95 Fixes for Erlang/OTP 23 2020-02-26 12:06:08 +01:00
Eric Meadows-Jönsson 184b71956d Fix float binary type specifier (#9844) 2020-02-25 20:54:11 +01:00
Wojtek Mach ca33a6c8a9 Code.fetch_docs/1: Support reading from doc/chunks/*.chunk (#9838) 2020-02-25 09:40:46 +01:00
David Bernheisel 0008320ac6 Update DateTime.from_unix documentation (#9840)
The documentation stated that negative numbers down to -62167219200 are
supported, but it actually works down to -377705116800. Add doctests to
illustrate the upper and lower boundaries.
2020-02-24 23:16:18 +01:00
José Valim 3c9e004ded Rewrite and deprecate System.stacktrace 2020-02-24 21:31:37 +01:00
José Valim da9b901457 Pass the documentation format to IO.ANSI.print/3 2020-02-24 14:40:03 +01:00
José Valim 1a2f5e5828 Fix the build 2020-02-24 13:04:46 +01:00
José Valim 276bd54f45 Also use markdown docs formatter when ANSI is disabled 2020-02-24 13:03:50 +01:00
José Valim 610770d227 Link to tz library too 2020-02-24 09:46:39 +01:00
José Valim 413f608ed4 Do not show binary types if they are inferred 2020-02-23 10:26:11 +01:00
James Grant a07c39b277 Fix typo in "Patterns and Guards.md" (#9836) 2020-02-23 01:00:29 +01:00
Johan Mattisson a5effa54a3 Clarify Map documentation regarding map.key syntax (#9834) 2020-02-22 13:29:18 +01:00
José Valim 182f16b76f Avoid false positives on capture warning 2020-02-20 21:50:47 +01:00
Eric Meadows-Jönsson 0f85b397da Add checking of patterns in body (#9828) 2020-02-20 16:46:06 +01:00
José Valim af4d6c3731 Revert "Support import Mod, only: :sigils (#9822)"
It will be moved to the current multi-letter sigils PR.

This reverts commit 26adc8db3d.
2020-02-19 10:42:48 +01:00
José Valim 7962c6859a Normalize literal functions during expansion 2020-02-18 14:46:45 +01:00
Wojtek Mach 9d74c6a718 Use simpler example for doctests and exceptions (#9827) 2020-02-17 21:56:16 +01:00
José Valim 4de2cd7d95 Clarify inspect docs 2020-02-17 17:21:52 +01:00
José Valim fa686ee858 Revert "No longer convert file name metadata upfront"
This would be a backwards incompatible change.

This reverts commit 3b1a14190e.
2020-02-17 17:02:42 +01:00
Wojtek Mach 26adc8db3d Support import Mod, only: :sigils (#9822) 2020-02-17 16:58:09 +01:00
Łukasz Jan Niemier a188258633 Improve Git diffs (#9823)
In Git 2.25 there comes native support for Elixir definitions. That will
make `git diff --function-context` work properly. Additionally it makes
diff headers to contain name of enclosing function.

I hope that this will work in GitHub diffs as well.
2020-02-17 08:13:58 +01:00
José Valim 96d63a1ec9 Do not treat two empty lists as charlists, closes #9824 2020-02-16 21:02:19 +01:00
Wojtek Mach 711f346a44 Fix error message for import :except option (#9819)
Before this patch we had:

    iex> import Kernel, except: :macros
    ** (CompileError) iex:1: invalid :except option for import, expected value to be an atom :functions, :macros, or a list literal, got: :macros
        (elixir 1.11.0-dev) src/elixir_import.erl:84: :elixir_import.calculate/6
        (elixir 1.11.0-dev) src/elixir_import.erl:24: :elixir_import.import/4
2020-02-16 17:35:15 +01:00
Wojtek Mach 628bf60c55 Do not compile erlang files with warnings_as_errors unless on CI (#9821) 2020-02-16 16:23:06 +01:00
Bram Verburg 48b9418307 Clarify System.cmd/2 environment handling (#9820) 2020-02-16 10:17:15 +01:00
José Valim 5984c6cc29 Revert "Warn on use of non-match argument to with/1 (#9784)"
There are too many false negatives to make this warning worth it.

This reverts commit 35665e7aac.
2020-02-15 22:02:40 +01:00
Gianluca Nitti 6aa5e4b2a9 Use acc type alias in spec of Enum.reduce/3 (#9817) 2020-02-14 17:43:53 +01:00
Eric Meadows-Jönsson 30c5f49647 Fix divergedreq error when update changes requirement and version (#9811)
The fix is to mark the dependency for compilation in the Fetcher (by
touching .fetch) so that the Loader wont read the version from the .app
file.
2020-02-13 21:28:29 +01:00
José Valim 4cfad26915 Fix bootstrap 2020-02-13 12:16:05 +01:00
José Valim ad728a48ed Only escape contents outside of interpolation, closes #9812 2020-02-12 20:51:51 +01:00
José Valim 3b1a14190e No longer convert file name metadata upfront 2020-02-12 20:27:36 +01:00
José Valim d490c1297d Only print metadata of known types, closes #9814 2020-02-12 19:03:53 +01:00
José Valim 75e58e3ddc Do not crash on fake down messages on stream monitor, closes #9803 2020-02-12 18:31:17 +01:00
José Valim 4f1f5b8a47 Do not write files to disk if warnings as errors was triggered, closes #9808 2020-02-12 18:00:05 +01:00
Eric Meadows-Jönsson 27bd9ffcc6 Optimize Version.match?/2 and Version.compare/2 (#9807)
Pattern matching turns out to be faster than using matchspecs.

string = "~> 1.2.3 and ~> 1.0"
requirement = Version.parse_requirement!(string)
compiled_requirement = Version.compile_requirement(requirement)
new_requirement = Version.new_requirement(string)

versions =
  for major <- 1..10,
      minor <- 1..10,
      patch <- 1..10,
      pre <- [[], ["dev"]],
      do: %Version{major: major, minor: minor, patch: patch, pre: pre, build: nil}

Benchee.run(
  %{
    "old" => fn -> Enum.map(versions, &Version.match?(&1, requirement)) end,
    "old & compiled" => fn -> Enum.map(versions, &Version.match?(&1, compiled_requirement)) end,
    "new" => fn -> Enum.map(versions, &Version.new_match?(&1, new_requirement)) end
  },
  time: 10,
  memory_time: 2
)

Operating System: macOS
CPU Information: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
Number of Available Cores: 12
Available memory: 16 GB
Elixir 1.11.0-dev
Erlang 22.0

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 10 s
memory time: 2 s
parallel: 1
inputs: none specified
Estimated total run time: 42 s

Benchmarking old & compiled...
Benchmarking new...
Benchmarking old...

Name                   ips        average  deviation         median         99th %
new                 5.93 K      168.77 μs    ±11.60%      165.99 μs      266.99 μs
old & compiled      1.18 K      849.40 μs     ±7.78%      834.99 μs     1177.37 μs
old               0.0400 K    25012.15 μs     ±2.98%    24798.47 μs    27617.74 μs

Comparison:
new                 5.93 K
old & compiled      1.18 K - 5.03x slower +680.63 μs
old               0.0400 K - 148.20x slower +24843.38 μs

Memory usage statistics:

Name            Memory usage
new                125.05 KB
old & compiled     157.52 KB - 1.26x memory usage +32.46 KB
old                250.99 KB - 2.01x memory usage +125.94 KB

**All measurements for memory usage were the same**
2020-02-11 14:28:40 +01:00
José Valim cc8093aad9 According to systools we only need to copy bin from erts 2020-02-10 10:49:41 +01:00
José Valim b2ee70ee74 Unpack extra guards from with matches, closes #9801 2020-02-09 18:15:21 +01:00
José Valim f2cb13c39b Clarify Access docs 2020-02-09 12:57:22 +01:00
José Valim 4abd1128fe Update TODO notes 2020-02-09 09:44:46 +01:00
Łukasz Jan Niemier 1fa3c45d1b Expose all logger levels (#9791) 2020-02-09 09:40:56 +01:00
Łukasz Jan Niemier 6311832079 fix(logger): use correct return value from report_cb/2 (#9798)
Earlier it assumed that the return type is the same as in `report_cb/1`
which is not true. `report_cb/1` returns format string and data while
`report_cb/2` returns already formatted string.
2020-02-09 09:34:34 +01:00
José Valim 840f52077a Ensure assert_received still produces valid exceptions, closes #9799 2020-02-09 09:32:50 +01:00
sanposhiho 5e0a47f936 Add guards to functions in Tuple (#9796) 2020-02-08 09:21:22 +01:00
José Valim cd97da3c41 Fix CI 2020-02-08 08:41:38 +01:00
José Valim acd9bf6a94 Fix typo 2020-02-08 08:31:05 +01:00
José Valim 10ef56fdfa Do not imply nil is caused by Access 2020-02-08 01:58:37 +01:00
José Valim 8cdaeb049a Do not emit warnings for variables used in bin patterns 2020-02-07 15:28:59 +01:00
José Valim b481ed6668 Do not require parens in IEx 2020-02-07 12:47:55 +01:00
Wojtek Mach 480c3a30be Fix unused variable warnings (#9790) 2020-02-06 20:27:47 +01:00
José Valim 3ea9abaca0 Add missing :erlang.size/1 guard, closes #9789 2020-02-06 18:44:49 +01:00
José Valim 31f8291895 Emit warnings for unused match vars in assertions, closes #9787 2020-02-06 18:35:30 +01:00
Andrea Leopardi aa55a5bfd6 Improve documentation for ExUnit callbacks (#9785) 2020-02-05 18:50:32 +02:00
Andrea Leopardi fe09be743f Fix a link to a type in the docs for Logger
[ci skip]
2020-02-05 14:21:26 +01:00
Jesse Van Volkinburg 35665e7aac Warn on use of non-match argument to with/1 (#9784)
Closes #9764.
2020-02-05 13:14:55 +02:00
José Valim 53dbf7083e Add Code.can_await_module_compilation?/0 (#9777) 2020-02-05 11:39:08 +01:00
José Valim a1103711db Emit each file on verbose elixirc 2020-02-05 09:04:18 +01:00
Łukasz Jan Niemier 4015986310 Use Erlang's logger timestamp (#9772)
Instead of generating new timestamp on each log message use existing
`time` entry from `:os.system_time/1`.

Ref #9465
2020-02-05 09:03:51 +01:00
Eric Meadows-Jönsson 0d32ea6bc4 Add IO.warn_once/3 (#9782)
Only warn once in Supervisor.start_child/2.
2020-02-05 01:06:58 +01:00
José Valim b9e2d38bb6 Only warn for commas at the end of words 2020-02-04 13:25:02 +01:00
Joel C 2532c37d15 Quote variables in cli_bat_text install section (#9780)
This allows the release batch script to be run in a path that
includes spaces.
2020-02-04 11:22:53 +01:00
José Valim c31958aa17 Add tests for timer and interval with infinity 2020-02-03 12:42:05 +01:00
Fernando Tapia Rico 02abfb75fc Remove unneeded code in test (#9778) 2020-02-03 11:23:51 +01:00
José Valim 49fcc6522c Add missing since annotations 2020-02-03 10:36:01 +01:00
José Valim 1ce5b6a311 Run formatter 2020-02-03 09:46:48 +01:00
José Valim 8715a7215e Remove redundant process test
The previous test already checked trapping exits
returns the exit message.
2020-02-03 09:09:00 +01:00
José Valim e5a85b5fb8 Update CHANGELOG 2020-02-03 08:37:25 +01:00
Masashi Yokota fc5afad35d Optimize pattern match of access module's func (#9776) 2020-02-02 19:02:05 +01:00
José Valim 89bc4aff6c Raise proper error message for invalid strftime format 2020-02-02 10:03:00 +01:00
Ganesh Sarwate 0a93a6569e Escape Unicode chars in doucmentation for String codepoints/1 (#9775) 2020-02-02 09:26:19 +01:00
Piotr Szmielew 66f13b7051 Deprecate remote nil arity calls without parens (#9769) 2020-02-01 20:54:45 +01:00
José Valim 534d8eac6b Do not emit invalid code when formatting assocs in maps, closes #9771 2020-02-01 11:18:28 +01:00
Akash Hiremath 172fc603ed Fix logger truncate for multi-byte characters (#9767) 2020-01-31 10:44:30 +01:00
Mathieu Decaffmeyer 9f2962253f Fix doc for time_zone_periods_from_wall_datetime/2 (#9770) 2020-01-31 09:07:51 +01:00
Mariusz Gumienny 41b211c0db Fix typo in Patterns and Guards page (#9768) 2020-01-30 20:06:08 +01:00
José Valim 9b4b1c9712 Update ci_email.yml 2020-01-30 17:07:32 +01:00
José Valim ea42a4077c No longer rebroadcast messages as this is taken care by Erlang's logger
Closes #9765.
2020-01-30 17:02:15 +01:00
José Valim b62a23d521 Deprecations are automatically detected in ExDoc master 2020-01-30 12:42:45 +01:00
Piotr Szmielew 4460b53e1f Allow using remote or rpc with compile_env
Signed-off-by: José Valim <jose.valim@dashbit.co>
2020-01-30 12:05:17 +01:00
Devon Estes 12c91a2780 Fix docs for compiler tracing events (#9760)
The `local_function` event is a tuple with four elements, not five.
2020-01-29 13:50:59 +01:00
Devon Estes 806204c732 Allow users to tag doctests with options (#9754) 2020-01-28 09:56:13 +01:00
Ian Young 77801a9eb7 Fix typo in String documentation (#9756) 2020-01-28 07:36:51 +01:00
José Valim 93a3c5f958 Do not allow bad lines at runtime 2020-01-27 15:37:32 +01:00
José Valim 20d4eb1c44 Add space to quote error message 2020-01-27 12:50:16 +01:00
José Valim 25474c6c07 Improve failure stacktrace by avoiding tail recursion 2020-01-25 18:01:14 +01:00
Deniz Adrian 0098f21243 Ensure ERTS is not copied into release (#9752)
The default behaviour when starting a new mix project is to
`include_erts: true`.

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

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

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

This also adds logic to not try to include ERTS into archives in the
first place when our release is configured with `include_erts: false`.
2020-01-25 15:43:44 +01:00
José Valim caf642f311 Updates to Calendar.stfrtime/3 2020-01-24 10:34:06 +01:00
Gustavo 0f2c0b3fd2 Turn NimbleStrftime into a Calendar function (#9747) 2020-01-24 09:04:05 +01:00
José Valim 6092ee1ff1 Print conclusion when sending e-mail 2020-01-24 00:22:08 +01:00
José Valim 35669c9f71 Dump GitHub context 2020-01-23 23:52:44 +01:00
José Valim 5918b24d72 Allow file, line and context to be dynamically set on quote, closes #9721 2020-01-23 20:26:16 +01:00
José Valim f1abf15a1a Use check_suite key 2020-01-23 18:36:43 +01:00
José Valim e6ac381e07 Only run check when completed 2020-01-23 17:30:45 +01:00
José Valim 18167b811c Use Github actions e-mail (#9750) 2020-01-23 17:25:54 +01:00
José Valim 43e3b6ebee Update return types in docs for Mix.Generator functions, closes #9749 2020-01-23 14:40:01 +01:00
Eric Meadows-Jönsson 1d7af251ad Only error for duplicate map keys in matches (#9748)
In matches we will error:

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

In normal expressions we will only warn:

    %{a: :b, a: :c}
2020-01-21 18:15:35 +01:00
Everett Griffiths 391f8c9e8b Adds links and examples to docs around logging (#9743) 2020-01-21 17:43:07 +01:00
José Valim b2de6d8cd8 Clear up all monitors on async_many failures 2020-01-19 13:21:23 +01:00
Ian Young 86df142ed0 Add Task.await_many/2 (#9741) 2020-01-19 13:14:03 +01:00
Devon Estes ed87925a68 Move evaluation of default timeout in ExUnit to runtime (#9745)
There were some confusing errors around this evaluation of the default
timeout at compile time, so now it's been moved to runtime.
2020-01-17 21:42:23 +01:00
José Valim ef61614f60 Keep existing APIs for min/max/min_by/max_by 2020-01-17 12:54:31 +01:00
Everett Griffiths 2cb4b77f40 Improve example and add "implicit try" phrase (#9740) 2020-01-15 22:12:59 +01:00
Fernando Tapia Rico 58a551f338 Update Library Guidelines.md (#9739) 2020-01-15 19:00:34 +01:00
José Valim a4450fa8b2 Add a new anti-pattern section to library guidelines 2020-01-15 17:56:03 +01:00
Wojtek Mach 7287ae758a Fix bug: move :indentation from sigil meta to binary meta (#9738)
We need it there so we can retrieve it like this:

    defmacro sigil_E({:<<>>, meta, [expr]}, []) do
      options = [
        line: __CALLER__.line + 1,
        indentation: meta[:indentation] || 0
      ]

      EEx.compile_string(expr, options)
    end
2020-01-15 16:11:38 +01:00
Wojtek Mach 81f59ec775 Add column to EEx.SyntaxError (#9734) 2020-01-15 12:13:33 +01:00
José Valim 21ed5f20e9 Tidy up Mix tasks docs 2020-01-15 09:19:01 +01:00
José Valim 0b3da17c11 Increase memory on Windows builds (#9731) 2020-01-15 09:13:29 +01:00
Everett Griffiths 577e3b997b Improves docs for tasks and shell (#9735) 2020-01-15 09:13:04 +01:00
Adriano Mitre 1c8e3f3cec Refactor invalid line numbers in ExUnit filters (#9733) 2020-01-14 23:48:19 +01:00
Wojtek Mach 331df3587b Add :indentation to sigil heredoc meta (#9725) 2020-01-14 20:45:24 +01:00
Wojtek Mach 94d9ea29a4 Add :indentation option to EEx (#9724) 2020-01-14 20:41:54 +01:00
Xavier Noria 52ee3ac163 Fixed-width font for Kernel [skip ci] (#9732) 2020-01-14 18:48:14 +01:00
Eric Meadows-Jönsson d8d8eafc52 Keep line number in cond_clause error (#9730) 2020-01-14 17:53:25 +01:00
Devon Estes 8a144112cc Reject invalid line numbers in ExUnit filters (#9729)
For line numbers that are 0 or less, we show a warning and reject those bad line numbers.
2020-01-14 17:52:02 +01:00
Fernando Tapia Rico 14f7f797be Clarify dirs can also be given as args in mix test (#9728) 2020-01-14 10:52:08 +01:00
Po Chen 09ad598a12 Fix since for is_struct/2 (#9726) 2020-01-13 23:33:03 +01:00
Wojtek Mach c5f735c989 Add since to new :column option on string_to_quoted (#9723) 2020-01-13 18:36:59 +01:00
José Valim 829fe98403 Update CHANGELOG 2020-01-13 15:33:10 +01:00
Łukasz Jan Niemier a189bb24f1 Add is_struct/2 macro (#9718) 2020-01-13 09:37:53 +01:00
Jonny Stoten aaae97992d Don't GPG sign commits or tags in git tests (#9719)
If the machine/user git config is set to sign all commits/tags,
these tests fail.
2020-01-13 09:37:20 +01:00
José Valim 216bf94577 Do not expand interpolation twice, closes #9709 2020-01-12 17:19:50 +01:00
José Valim 925cba060c Make sure to traverse args in xref 2020-01-12 16:25:02 +01:00
José Valim b75f5899e1 Remove guard checks on is_odd/is_even as those are tested in defguard, closes #9700 2020-01-12 15:59:59 +01:00
Wojtek Mach 4436bc4b19 Track column information in EEx if enabled 2020-01-12 15:41:36 +01:00
Wojtek Mach 8364149953 Add :column to Code.string_to_quoted*/2 2020-01-12 15:41:36 +01:00
Pete Matsyburka 45fd2a6fac Add :. to special forms (#9717) 2020-01-12 15:37:09 +01:00
Eric Meadows-Jönsson b78d68db32 Fix formatting 2020-01-12 13:24:58 +01:00
Eric Meadows-Jönsson e8c49a446a Do not warn for functions generated by Erlang compiler (#9713) 2020-01-12 12:15:21 +01:00
Eric Meadows-Jönsson c7f0295d6d Support map.field syntax in guards (#9711) 2020-01-11 16:31:56 +01:00
José Valim 6fd3f52f05 Use meta on rewrites, closes #9714 2020-01-11 12:17:08 +01:00
José Valim e69eb8f172 Move mix xref graph --format stats to mix xref stats 2020-01-11 11:23:33 +01:00
Eric Meadows-Jönsson 9a627b2f5e Fix no_parens test 2020-01-11 02:24:04 +01:00
José Valim 9a1d52cb4f Add more no_parens check to quoted 2020-01-10 09:24:09 +01:00
José Valim 7a159a049c Keep no_parens distinction in quote/unquote 2020-01-10 09:03:07 +01:00
Eric Meadows-Jönsson b6f4f4f2f3 Allow any type on the RHS of or/2 (#9706)
Closes #9704.
2020-01-09 17:07:18 +01:00
Eric Meadows-Jönsson c4db8841ab Improve type check error message (#9705)
A type error may not always cause a clause to fail.

Related to #9700.
2020-01-09 15:17:19 +01:00
José Valim 80ae2d4980 Test copy_erts with string and without source 2020-01-09 10:06:54 +01:00
Tobiasz Małecki 808ccb93cf Fix mix.release raising error when src is missing in erts_source (#9703) 2020-01-09 09:53:07 +01:00
José Valim 2cab7ec926 Also remove undefined warnings when loading projects outside the CLI 2020-01-08 11:52:21 +01:00
Hassan Schroeder 06c2ab093d Clarify which home directory is referred to by ~ in ~/.iex.exs (#9699) 2020-01-08 10:30:34 +01:00
José Valim d002f2fd10 Deprecate remaining supervisor API 2020-01-07 18:17:34 +01:00
José Valim 76b804f725 Start v1.11 2020-01-07 15:06:25 +01:00
350 changed files with 18971 additions and 8054 deletions
+3 -122
View File
@@ -2,139 +2,21 @@ env:
CIRRUS_CLONE_DEPTH: 50
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "+warnings_as_errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
test_template: &DEFAULT_TEST_SETTINGS
# don't cancel the task execution if it's master or a release branch
auto_cancellation: $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH !=~ 'v\d+\.\d+.*'
test_linux_task:
<<: *DEFAULT_TEST_SETTINGS
container:
image: buildpack-deps:trusty
cpu: 8
memory: 1536Mi
env:
PATH: "${CIRRUS_WORKING_DIR}/otp/bin:${PATH}"
matrix:
- name: Linux, ${OTP_RELEASE}, Ubuntu 14.04
alias: Linux Stable
matrix:
- env:
CHECK_POSIX_COMPLIANT: true
CHECK_REPRODUCIBLE: true
OTP_RELEASE: OTP-22.1
- env:
OTP_RELEASE: OTP-22.0
- env:
OTP_RELEASE: OTP-21.3.8
- env:
OTP_RELEASE: OTP-21.2
- env:
OTP_RELEASE: OTP-21.1
- env:
OTP_RELEASE: OTP-21.0
- name: Linux, OTP-${OTP_RELEASE}, development, Ubuntu 14.04
alias: Linux Development
allow_failures: true
skip_notifications: true
depends_on:
- Linux Stable
- FreeBSD Stable
matrix:
- env:
OTP_RELEASE: master
- env:
OTP_RELEASE: maint
install_script:
- wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${OTP_RELEASE}.tar.gz
- mkdir -p otp
- tar zxf otp.tar.gz -C otp --strip-components=1
- otp/Install -minimal ${CIRRUS_WORKING_DIR}/otp
- rm -rf .git
- make compile
build_info_script: bin/elixir --version
test_formatted_script:
- make test_formatted &&
echo "All Elixir source code files are properly formatted."
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
test_erlang_script: make test_erlang
test_elixir_script: make test_elixir
check_posix_compliant_script: |
if [ -n "$CHECK_POSIX_COMPLIANT" ]; then
apt update
apt install -y shellcheck
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
else
echo "The format of the shell scripts is only checked in the last stable Erlang/OTP version."
fi
check_reproducible_script: |
if [ -n "$CHECK_REPRODUCIBLE" ]; then
make check_reproducible
else
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
fi
test_windows_task:
<<: *DEFAULT_TEST_SETTINGS
name: Windows, OTP-${OTP_RELEASE}, Windows Server 2019
alias: Windows Stable
matrix:
- env:
OS_VERSION: 2019
OTP_RELEASE: 22.0
- env:
OS_VERSION: 2019
OTP_RELEASE: 21.0.1
windows_container:
image: fertapric/elixir-ci:otp-win64-${OTP_RELEASE}
os_version: ${OS_VERSION}
cpu: 4
memory: 3840Mi
install_script:
- rmdir /s /q .git
- make compile
build_info_script: bin/elixir --version
test_formatted_script:
- make test_formatted &&
echo "All Elixir source code files are properly formatted."
test_erlang_script: make --keep-going test_erlang
test_elixir_script: make --keep-going test_elixir
test_freebsd_task:
<<: *DEFAULT_TEST_SETTINGS
name: FreeBSD 12.0
name: FreeBSD 12.1
alias: FreeBSD Stable
freebsd_instance:
image_family: freebsd-12-0
image_family: freebsd-12-1
cpu: 8
memory: 7424Mi
@@ -143,7 +25,6 @@ test_freebsd_task:
LC_ALL: en_US.UTF-8
install_script:
- sudo pkg update
- pkg install -y erlang git gmake
- rm -rf .git
- gmake compile
+2
View File
@@ -1 +1,3 @@
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
*.ex diff=elixir
*.exs diff=elixir
+102
View File
@@ -0,0 +1,102 @@
name: CI
on: [pull_request, push]
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
jobs:
test_linux:
name: Linux, ${{ matrix.otp_release }}, Ubuntu 16.04
continue-on-error: ${{ matrix.development }}
strategy:
matrix:
otp_release: ['OTP-23.0', 'OTP-22.3', 'OTP-22.0', 'OTP-21.3.8', 'OTP-21.0']
development: [false]
include:
- otp_release: master
development: true
- otp_release: maint
development: true
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 50
- name: Install Erlang/OTP
run: |
cd $RUNNER_TEMP
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${{ matrix.otp_release }}.tar.gz
mkdir -p otp
tar zxf otp.tar.gz -C otp --strip-components=1
otp/Install -minimal $(pwd)/otp
echo "::add-path::$(pwd)/otp/bin"
- name: Compile Elixir
run: |
rm -rf .git
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Dyalizer
run: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
- name: Erlang test suite
run: make test_erlang
- name: Elixir test suite
run: make test_elixir
- name: Check reproducible builds
run: taskset 1 make check_reproducible
if: matrix.otp_release == 'OTP-23.0'
test_windows:
name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019
strategy:
matrix:
otp_release: ['22.0']
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@v1
with:
fetch-depth: 50
- name: Cache Erlang/OTP package
uses: actions/cache@v2
with:
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\erlang
key: OTP-${{ matrix.otp_release }}-windows-2019
- name: Install Erlang/OTP
run: choco install -y erlang --version ${{ matrix.otp_release }}
- name: Compile Elixir
run: |
remove-item '.git' -recurse -force
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make --keep-going test_erlang
- name: Elixir test suite
run: make --keep-going test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 50
- name: Install Shellcheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Check POSIX-compliant
run: |
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
-17
View File
@@ -1,17 +0,0 @@
on: check_suite
name: CI email
jobs:
sendEmail:
name: Send email
runs-on: ubuntu-latest
steps:
- name: Send email
# Source: https://github.com/elixir-lang/elixir-ci
uses: docker://fertapric/elixir-ci-email:latest
env:
APP_NAME: Cirrus CI
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAIL_FROM: ci@elixir-lang.org
MAIL_HOST: smtp.sendgrid.net
MAIL_PASSWORD: ${{ secrets.CI_EMAIL_PASSWORD }}
MAIL_USERNAME: ${{ secrets.CI_EMAIL_USERNAME }}
+347 -135
View File
@@ -1,222 +1,434 @@
# Changelog for Elixir v1.10
# Changelog for Elixir v1.11
## Support for Erlang/OTP 21+
Over the last releases, the Elixir team has been focusing on the compiler, both in terms of catching more mistakes at compilation time and making it faster. Elixir v1.11 has made excellent progress on both fronts. This release also includes many other goodies, such as tighter Erlang integration, support for more guard expressions, built-in datetime formatting, and other calendar enhancements.
Elixir v1.10 requires Erlang/OTP 21+, allowing Elixir to integrate with Erlang/OTP's new logger. Currently, this means that the logger level, logger metadata, as well as all log messages are now shared between Erlang and Elixir APIs.
## Tighter Erlang integration
We will continue improving the relationship between the logging systems in future releases. In particular, we plan to expose all log levels and runtime filtering functionalities available in Erlang directly into Elixir in the next Elixir version.
Following Elixir v1.10, we have further integrated with Erlang's new logger by adding four new log levels: `notice`, `critical`, `alert`, and `emergency`, matching all log levels found in the Syslog standard. The `Logger` module now supports structured logging by passing maps and keyword lists to its various functions. It is also possible to specify the log level per module, via the `Logger.put_module_level/2` function. Log levels per application will be added in future releases.
This release also adds two new guards, `is_struct/1` and `is_map_key/2`, thanks to the strict requirement on Erlang/OTP 21+.
IEx also has been improved to show the documentation for Erlang modules directly from your Elixir terminal. This works with Erlang/OTP 23+ and requires Erlang modules to have been compiled with documentation chunks.
## Releases improvements
## Compiler checks: application boundaries
Elixir v1.9 introduced releases as a mechanism to package self-contained applications. Elixir v1.10 further improves releases with bug fixes and new enhancements based on feedback we got from the community. The highlights are:
Elixir v1.11 builds on top of the recently added compilation tracers to track application boundaries. From this release, Elixir will warn if you invoke a function from an existing module but this module does not belong to any of your listed dependencies.
* Allow the dual boot system of releases to be disabled on environments that are boot-time sensitive, such as embedded devices
These two conditions may seem contradictory. After all, if a module is available, it must have come from a dependency. This is not true in two scenarios:
* Track and raise if compile-time configuration is set or changes at runtime (more in the next section)
* Modules from Elixir and Erlang/OTP are always available - even if their applications are not explicitly listed as a dependency
* Support for easily adding extra files to releases via overlays
* In an umbrella project, because all child applications are compiled within the same VM, you may have a module from a sibling project available, even if you don't depend on said sibling
* Allow `RELEASE_DISTRIBUTION` to be set to `none` in order to fully disable it
This new compiler check makes sure that all modules that you invoke are listed as part of your dependencies, emitting a warning like below otherwise:
* Add a built-in `:tar` step that automatically packages releases
```text
:ssl.connect/2 defined in application :ssl is used by the current
application but the current application does not directly depend
on :ssl. To fix this, you must do one of:
See the full CHANGELOG for more improvements.
1. If :ssl is part of Erlang/Elixir, you must include it under
:extra_applications inside "def application" in your mix.exs
## Improvements to sort-based APIs in Enum
2. If :ssl is a dependency, make sure it is listed under "def deps"
in your mix.exs
`Enum.sort/1` in Elixir always sorts from lowest to highest. If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code.
3. In case you don't want to add a requirement to :ssl, you may
optionally skip this warning by adding [xref: [exclude: :ssl]
to your "def project" in mix.exs
```
To make matters worse, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result. Therefore, to sort dates from more recent to oldest, one has to write `Enum.sort(dates, &(Date.compare(&1, &2) != :lt))`.
This comes with extra benefits in umbrella projects, as it requires child applications to explicitly list their dependencies, completely rejecting cyclic dependencies between siblings.
Elixir v1.10 streamlines the sorting functions by introducing both `:asc` and `:desc` shortcuts:
## Compiler checks: data constructors
Enum.sort(collection, :asc) # the default
Enum.sort(collection, :desc) # in reverse
In Elixir v1.11, the compiler also tracks structs and maps fields across a function body. For example, imagine you wanted to write this code:
Furthermore, if you want to perform semantic comparison, you can pass a module that provides the relevant comparison function. For example, to sort dates:
def drive?(%User{age: age}), do: age >= 18
Enum.sort(birth_dates, Date)
Enum.sort(birth_dates, {:asc, Date})
Enum.sort(birth_dates, {:desc, Date})
If there is either a typo on the `:age` field or the `:age` field was not yet defined, the compiler will fail accordingly. However, if you wrote this code:
This new API has also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
def drive?(%User{} = user), do: user.age >= 18
### Tracking of compile-time configuration
The compiler would not catch the missing field and an error would only be raised at runtime. With v1.11, Elixir will track the usage of all maps and struct fields within the same function, emitting warnings for cases like above:
All applications in Elixir come with an application environment. This environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
```text
warning: undefined field `age` in expression:
defmodule MyApp.DBClient do
@db_host Application.get_env(:my_app, :db_host, "db.local")
# example.exs:7
user.age
def start_link() do
SomeLib.DBClient.start_link(host: @db_host)
end
expected one of the following fields: name, address
where "user" was given the type %User{} in:
# example.exs:7
%User{} = user
Conflict found at
example.exs:7: Check.drive?/1
```
The compiler also checks binary constructors. Consider you have to send a string over the wire with length-based encoding, where the string is prefixed by its length, up to 4MBs. Your initial attempt may be this:
def run_length(string) when is_binary(string) do
<<byte_size(string)::32, string>>
end
This approach has one big limitation: if you change the value of the application environment after the code is compiled, the value used at runtime is not going to change! For example, if you are using `mix release` and your `config/releases.exs` has:
However, the code above has a bug. Each segment given between `<<>>` must be an integer, unless specified otherwise. With Elixir v1.11, the compiler will let you know so:
config :my_app, :db_host, "db.production"
```text
warning: incompatible types:
The new value will have no effect as the code was compiled to connect to "db.local", which is mostly likely unavailable in the production environment.
binary() !~ integer()
For those reasons, reading the application environment at runtime should be the first choice. However, if you really have to read the application environment during compilation, Elixir v1.10 introduces a `Application.compile_env/3` function:
in expression:
@db_host Application.compile_env(:my_app, :db_host, "db.local")
<<byte_size(string)::integer()-size(32), string>>
By using `compile_env/3`, Elixir will store the values used during compilation and compare the compilation values with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
where "string" was given the type integer() in:
### Compiler tracing
# foo.exs:4
<<byte_size(string)::integer()-size(32), string>>
This release brings enhancements to the Elixir compiler and adds new capabilities for developers to listen to compilation events.
where "string" was given the type binary() in:
In previous Elixir releases, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project. Although developers could traverse this database, they often requested more events or more information to be made available.
# foo.exs:3
is_binary(string)
In Elixir v1.10, we have replaced this database by compiler tracing. This means that developers can now directly listen to events emitted by the compiler to store and collect all the information they need (and only the information they need).
HINT: all expressions given to binaries are assumed to be of type integer()
unless said otherwise. For example, <<expr>> assumes "expr" is an integer.
Pass a modifier, such as <<expr::float>> or <<expr::binary>>, to change the
default behaviour.
Elixir itself is already using the new compiler tracing to provide new functionality. In particular, the compiler now checks for undefined function warnings more consistently. In previous versions, we would emit undefined function warnings only for files in `lib`, skipping test files and scripts.
Conflict found at
foo.exs:4: Check.run_length/1
```
Furthermore, in Elixir v1.10 developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
Which can be fixed by adding `::binary` to the second component:
@compile {:no_warn_undefined, OptionalDependency}
defdelegate my_function_call(arg), to: OptionalDependency
def run_length(string) when is_binary(string) do
<<byte_size(string)::32, string::binary>>
end
Finally, as consequence of these improvements, some functionality related to `xref` (our previous database), has been deprecated in favor of the new compiler tracing.
While some of those warnings could be automatically fixed by the compiler, future versions will also perform those checks across functions and potentially across modules, where automatic fixes wouldn't be desired (nor possible).
### Other enhancements
## Compilation time improvements
The calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
Elixir v1.11 features many improvements to how the compiler tracks file dependencies, such that touching one file causes less files to be recompiled. In previous versions, Elixir tracked three types of dependencies:
There are many improvements related to the Elixir AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information has already been available to the Elixir formatter for a couple versions and has now been made public. Furthermore, all public metadata entries in the AST nodes have been extensively documented. These changes alongside the compiler improvements from previous section means tools like Credo and Boundary now have a better foundation to analyze the source code.
* compile time dependencies - if A depends on B at compile time, such as by using a macro, whenever B changes, A is recompiled
* struct dependencies - if A depends on B's struct, whenever B's struct definition changed, A is recompiled
* runtime dependencies - if A depends on B at runtime, A is never recompiled
Finally, ExUnit comes with two small but important improvements: `ExUnit.CaptureIO` can now be used in tests that run asynchronously and we have added "data-structure diffing" when performing assertions with pattern matching. So now, whenever an assertion such `assert %{field: value} = expression()` fails, ExUnit will show both left-hand and right-hand sides, highlighting the parts that did not match in red.
However, because dependencies are transitive, if A depends on B at compile time and B depends on C at runtime, A would depend on C at compile time. Therefore, it is very important to reduce the amount of compile time dependencies.
## v1.10.0
Elixir v1.11 replaces "struct dependencies" by "exports dependencies". In other words, if A depends on B, whenever B public's interface changes, A is recompiled. B's public interface is made by its struct definition and all of its public functions and macros.
### 1. Enhancements
This change allows us to mark `import`s and `require`s as "exports dependencies" instead of "compile time" dependencies. This simplifies the dependency graph considerably. For example, [in the Hex.pm project](https://github.com/hexpm/hexpm), changing the `user.ex` file in Elixir v1.10 would emit this:
```text
$ touch lib/hexpm/accounts/user.ex && mix compile
Compiling 90 files (.ex)
```
In Elixir v1.11, we now get:
```text
$ touch lib/hexpm/accounts/user.ex && mix compile
Compiling 16 files (.ex)
```
To make things even better, Elixir v1.11 also introduces a more granular file tracking for path dependencies. In previous versions, a module from a path dependency would always be treated as a compile time dependency. This often meant that if you have an umbrella project, changing an application would cause many modules in sibling applications to recompile. Fortunately, Elixir v1.11 will tag modules from dependencies as exports if appropriate, yielding dramatic improvements to those using path dependencies.
To round up the list of compiler enhancements, the `--profile=time` option added in Elixir v1.10 now also includes the time to compile each individual file. For example, in the Plug project, one can now get:
```text
[profile] lib/plug/conn.ex compiled in 935ms
[profile] lib/plug/ssl.ex compiled in 147ms (plus 744ms waiting)
[profile] lib/plug/static.ex compiled in 238ms (plus 654ms waiting)
[profile] lib/plug/csrf_protection.ex compiled in 237ms (plus 790ms waiting)
[profile] lib/plug/debugger.ex compiled in 719ms (plus 947ms waiting)
[profile] Finished compilation cycle of 60 modules in 1802ms
[profile] Finished group pass check of 60 modules in 75ms
```
While implementing those features, we have also made the `--long-compilation-threshold` flag more precise. In previous versions, `--long-compilation-threshold` would consider both the time a file spent to compile and the time spent waiting on other files. In Elixir v1.11, it considers only the compilation time. This means less false positives and you can now effectively get all files that take longer than 2s to compile by passing `--long-compilation-threshold 2`.
## `mix xref graph` improvements
To bring visibility to the compiler tracking improvements described in the previous section, we have also added new features to `mix xref`. `mix xref` is a task that describes cross-references between files in your projects. The `mix xref graph` subsection focuses on the dependency graph between them.
First we have made the existing `--label` flag to consider transitive dependencies. Using `--sink FILE` and `--label compile` can be a powerful combo to find out which files will change whenever the given `FILE` changes. For example, in the Hex.pm project, we get:
```text
$ mix xref graph --sink lib/hexpm/accounts/user.ex --label compile
lib/hexpm/billing/hexpm.ex
└── lib/hexpm/billing/billing.ex (compile)
lib/hexpm/billing/local.ex
└── lib/hexpm/billing/billing.ex (compile)
lib/hexpm/emails/bamboo.ex
├── lib/hexpm/accounts/email.ex (compile)
└── lib/hexpm/accounts/user.ex (compile)
lib/hexpm/emails/emails.ex
└── lib/hexpm_web/views/email_view.ex (compile)
lib/hexpm_web/controllers/api/docs_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/key_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/organization_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/organization_user_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/owner_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/package_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/release_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/repository_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/api/retirement_controller.ex
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
lib/hexpm_web/controllers/blog_controller.ex
└── lib/hexpm_web/views/blog_view.ex (compile)
lib/hexpm_web/endpoint.ex
├── lib/hexpm_web/plug_parser.ex (compile)
└── lib/hexpm_web/session.ex (compile)
```
All the files at the root will recompile if `lib/hexpm/accounts/user.ex` changes. Their children describe the *why*. For example, the `repository_controller.ex` file will recompile if user changes because it has a compile time dependency on `auth_helpers.ex`, which depends on `user.ex`. This indirect compile time dependency is often the source of recompilations and Elixir v1.11 now makes it trivial to spot them, so they can be eventually addressed.
Another improvement to `mix xref graph` is the addition of `--format cycles`, which will print all cycles in your compilation dependency graph. A `--min-cycle-size` flag can be used if you want to discard short cycles.
## `config/runtime.exs` and `mix app.config`
Elixir v1.9 introduced a new configuration file, specific to releases, called `config/releases.exs`. A release is a self-contained artifact with the Erlang VM, Elixir and your application, ready to run in production.
The addition of `config/releases.exs` has been a very useful one but, unfortunately, it applies only to releases. Developers not using releases must use the `config/config.exs` file, which often loaded too early at compilation time. For any dynamic configuration, developers had to resort to third-party tools or workarounds to achieve the desired results.
Elixir v1.11 addresses this issue by introducing a new configuration file, called `config/runtime.exs`. This new configuration file is loaded exactly before your application starts, when the code is already fully compiled. It is loaded in development, test, and production, regardless if you are using Mix or releases. Therefore it provides a unified API for runtime configuration in Elixir.
`config/runtime.exs` works the same as any other configuration file. However, given `config/runtime.exs` is meant to run with or without Mix, developers must not use `Mix.env()` or `Mix.target()` in `config/runtime.exs`. Instead, they must use the new `config_env()` and `config_target()`, which have been added to the `Config` module.
While `config/releases.exs` will continue to be supported, developers can migrate to `config/runtime.exs` without loss of functionality. For example, a `config/releases.exs` file such as this one
```elixir
# config/releases.exs
import Config
config :foo, ...
config :bar, ...
```
could run as is as `config/runtime.exs`. However, given `config/runtime.exs` runs in all environments, you may want to restrict part of your configuration to the `:prod` environment:
```elixir
# config/runtime.exs
import Config
if config_env() == :prod do
config :foo, ...
config :bar, ...
end
```
If both files are available, releases will pick the now preferred `config/runtime.exs` instead of `config/releases.exs`.
To wrap it all up, `Mix` also includes a new task called `mix app.config`. This task loads all applications and configures them, without starting them. Whenever you write your own Mix tasks, you will typically want to invoke either `mix app.start` or `mix app.config` before running your own code. Which one is better depends if you want your applications running or only configured.
## Other improvements
Elixir v1.11 adds the `is_struct/2`, `is_exception/1`, and `is_exception/2` guards. It also adds support for the `map.field` syntax in guards.
The Calendar module ships with a new `Calendar.strftime/3` function, which provides datetime formatting based on the `strftime` format. The `Date` module got new functions for working with weeks and months, such as `Date.beginning_of_month/1` and `Date.end_of_week/2`. Finally, all calendar types got conversion functions from and to gregorian timestamps, such as `Date.from_gregorian_days/2` and `NaiveDateTime.to_gregorian_seconds/1`.
Mix also includes two new tasks: `mix app.config`, for application runtime configuration, and `mix test.coverage`, which generates aggregated coverage reports for umbrella projects and for test suites partitioned across processes.
## v1.11.1 (2020-10-16)
### 1. Bug fixes
#### Elixir
* [Application] Add `Application.compile_env/3` and `Application.compile_env!/2` for reading values at compilation time and tracking if they accidentally change during runtime
* [Calendar] Allow custom calendar representations in calendar sigils
* [Calendar] Add `c:Calendar.parse_time/1`, `c:Calendar.parse_date/1`, `c:Calendar.parse_naive_datetime/1` and `c:Calendar.parse_utc_datetime/1` callbacks to calendar behaviour
* [CLI] Add support for `NO_COLOR` environment variable
* [Code] Add `:token_metadata` and `:literal_encoder` support to `Code.string_to_quoted/2`
* [Code] Add compiler tracing to lift events done by the compiler
* [Code] Return `{:error, :unavailable}` in `Code.ensure_compiled/1` if module is in a deadlock
* [DateTime] Add `DateTime.now!/2` and `DateTime.shift_zone!/3`
* [Enum] Speed up getting one random element from enumerables
* [Enum] Add `Enum.frequencies/1`, `Enum.frequencies_by/2`, and `Enum.map_intersperse/2`
* [Enum] Allow a sorting function on `Enum.min/max/min_by/max_by`
* [Enum] Add `asc/desc` and `compare/1` support to `Enum.sort/2`
* [Exception] Add version alongside app names in stacktraces
* [Function] Add `Function.identity/1`
* [Kernel] Add `Kernel.is_struct/1` and `Kernel.is_map_key/2`
* [Kernel] Warn when function head comes immediately after the implementation instead of before the implementation
* [Kernel] Warn if duplicate key is found in struct declaration
* [Kernel] Print all undefined functions as warnings and then raise. This allows users to see all undefined calls at once, when it would otherwise require them to compile the code multiple times
* [Keyword] Add `Keyword.pop!/2` and `Keyword.pop_values/2`
* [Map] Add `Map.pop!/2`
* [MapSet] Optimize multiple operations
* [Module] Add `Module.has_attribute?/2`
* [Module] Add `@compile {:no_warn_undefined, mfa_or_module}` to turn off undefined function warnings
* [NaiveDateTime] Add `NaiveDateTime.local_now/0`
* [Record] Warn if duplicate key is found in record declaration
* [String] Update to Unicode 12.1
* [StringIO] Add `:encoding` option to StringIO and optimize `get_chars` operation
#### ExUnit
* [ExUnit.Assertions] Support diffs in pattern matching and in `assert_receive`
* [ExUnit.CaptureIO] Supports capturing named devices in asynchronous tests
* [Code] Ignore tracers if lexical tracker is dead or explicitly nil when evaling code with an environment
* [GenServer] Do not show warning when using `super` in `GenServer.child_spec/1`
* [Kernel] Do not crash when :reduce is set to `nil` in comprehensions
* [Kernel] Fix a scenario where undefined function warnings were not being emitted
#### IEx
* [IEx] Warn on circular file imports when loading default `.iex.exs`
* [IEx] Allow customization of the continuation prompt on IEx
* [IEx.Helpers] Properly handle tags inside typespec when showing Erlang docs
#### Logger
* [Logger] Allow `start_options` to be configured on Logger's GenEvent
* [Logger] Integrate Elixir's Logger with Erlang/OTP 21+'s logger. This means setting up the logger level in Elixir will automatically change the logger level for Erlang and vice-versa
* [Logger] Do not deadlock Logger if handler crashes on sync mode
#### Mix
* [mix compile] Add `--profile time` flag to profile compilation steps
* [mix deps.compile] Add `--skip-umbrella-apps` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
* [mix deps.unlock] Add `--check-unused` flag. The new flag raises if there are any unused dependencies in the lock file
* [mix release] Allow `RELEASE_DISTRIBUTION` to be set to `none`
* [mix release] Support overlays in `rel/overlays`
* [mix release] Allow configuration reboot to be disabled in releases
* [mix test] Add support for simple round-robin test partitioning across multiple machines
* [Mix.Project] Add `MIX_DEPS_PATH` environment variable for setting `:deps_path`
* [Mix.Project] Add `Mix.Project.deps_scms/1` that returns deps with their SCMs
* [Mix.Task] Add `Mix.Task.Compiler.after_compiler/2` callback, to simplify compilers that may need to run something at multiple steps
* [Mix] Add inet6 fallback to Mix usage of httpc
* [mix compile.app] Do not list apps that do not match the current target
## v1.11.0 (2020-10-06)
### 1. Enhancements
#### EEx
* [EEx] Track column information in EEx templates when enabled in the compiler
* [EEx] Show column information in EEx error messages
* [EEx] Support `:indentation` option when compiling EEx templates for proper column tracking
#### Elixir
* [Access] Add `Access.at!/1`
* [Calendar] Add `Calendar.strftime/3` for datetime formatting
* [Calendar] Add linear integer representations to Calendar modules: `Date.from_gregorian_days/2`, `Date.to_gregorian_days/1`, `NaiveDateTime.from_gregorian_seconds/3`, `NaiveDateTime.to_gregorian_seconds/1`, `Time.from_seconds_after_midnight/1`, and `Time.to_seconds_after_midnight/1`
* [Calendar] Add `new!` to Date/Time/NaiveDateTime/DateTime (`new` has also been added to `DateTime` for completeness)
* [Calendar] Support custom starting day of the week in `Date.day_of_week/2`
* [Calendar] Add `Date.beginning_of_month/1` and `Date.end_of_month/1`
* [Calendar] Add `Date.beginning_of_week/2` and `Date.end_of_week/2`
* [Code] Add `:column` to `Code.string_to_quoted*/2`
* [Code] Add `Code.can_await_module_compilation?/0` to check if the parallel compiler is enabled and it can await for other modules to be compiled
* [Config] Support `config_env/0` and `config_target/0` in `config` files
* [Config] Allow `import_config` to be disabled for some configuration files
* [Enum] Allow a sorting function on `Enum.min_max_by/3,4`, including the new `compare/2` conventions
* [Kernel] Add `is_struct/2` guard
* [Kernel] Add `is_exception/1` and `is_exception/2` guards
* [Kernel] Support `map.field` syntax in guards
* [Kernel] Add `+++` and `---` with right associativity to the list of custom operators
* [Kernel] Warn if a variable that looks like a compiler variable (such as `__MODULE__`) is unused
* [Kernel.ParallelCompiler] Report individual file compilation times when `profile: :time` is given
* [Kernel.ParallelCompiler] Improve precision of `:long_compilation_threshold` so it takes only compilation times into account (and not waiting times)
* [Registry] Add `Registry.delete_meta/2`
* [Task] Add `Task.await_many/2`
#### ExUnit
* [ExUnit] Add support for coloring on Windows 10 consoles/shells
* [ExUnit] Add `ExUnit.fetch_test_supervisor/0`
* [ExUnit] Add `@tag :tmp_dir` support to ExUnit. The temporary directory is automatically created and pruned before each test
* [ExUnit] Add file and line to ExUnit's `--trace`
* [ExUnit.Assertion] Allow receive timeouts to be computed at runtime
* [ExUnit.Case] Add `register_test/6` to speed up compilation of custom tests
* [ExUnit.Doctest] Allow users to add tags to doctests
#### IEx
* [IEx] Add support for coloring on Windows 10 consoles/shells
* [IEx.Helpers] Show docs from Erlang modules that have been compiled with the docs chunk
#### Logger
* [Logger] Add `notice`, `critical`, `alert`, and `emergency` log levels
* [Logger] Support structured logging by logging maps or keyword lists
* [Logger] Allow level to be set per module with `Logger.put_module_level/2`
* [Logger] Include `erl_level` in Logger's metadata
#### Mix
* [mix] Add `MIX_BUILD_ROOT` to config `_build` dir
* [mix] Introduce `MIX_XDG` as a simpler mechanism to opt-in to the XDG specification
* [mix] Allow requirements for a Mix task to be listed via the `@requirements` module attribute
* [mix] Allow optional dependencies to be defined in `:extra_applications` and `:applications`
* [mix app.config] Add new `mix app.config` task that compiles applications and loads runtime configuration
* [mix archive.install] Support `--repo` option on Hex packages
* [mix compile] Support the `__mix_recompile__?/0` callback for custom behaviour on when Mix should recompile a given module
* [mix compile.elixir] Mark modules for path dependencies as "Export dependencies" if they changed but their public interface is the same
* [mix compile.elixir] Track application boundaries in the Elixir compiler. If you invoke code from Erlang or Elixir standard libraries and you don't depend on the proper applications, a warning will be emitted. A warning will also be emitted if you invoke code from an umbrella sibling that you don't depend on - effectively forbidding cyclic dependencies between apps
* [mix deps] Sort the dependencies alphabetically before printing
* [mix deps] Use `origin/HEAD` as the default Git ref in dependencies
* [mix deps] Redact Git `username`/`password` in output log
* [mix deps] Support rebar3's `git_subdir` resource type
* [mix deps.compile] Allow local deps to be skipped on `mix deps.compile`
* [mix deps.unlock] Print which dependencies get unlocked when using the `--unused` flag
* [mix escript.install] Support `--repo` option on Hex packages
* [mix new] Add `@impl` to application generated by `mix new --sup`
* [mix release] Enable overriding `sys.config` location via `RELEASE_SYS_CONFIG` env var
* [mix release] Boot a release under configuration in interactive mode and then swap to embedded mode (if running on Erlang/OTP 23+)
* [mix release] Add `rel_templates_path` to configure the source of template files such as "env.sh.eex", "vm.args.eex" and "overlays"
* [mix release] Allow some chunks to be kept in the `:strip_beams` config
* [mix test] Allow `:ignore_modules` inside `:test_coverage` option
* [mix test.coverage] Add `mix test.coverage` that aggregates coverage results from umbrellas and OS partitioning
* [mix xref] Make the `--label` option for `mix xref graph` transitive by default and add `--only-direct` for only direct dependencies
* [mix xref] Add `--format cycles` support for `mix xref graph`
* [mix xref] Add support to `mix xref graph` for using `--source` and `--sink` at the same time
### 2. Bug fixes
#### EEx
* [EEx] Ensure multiline do/end with no spaces compile under trim mode
* [EEx] Make trimming behaviour via the `:trim` option more consistent
#### Elixir
* [Enum] Allow positive range slices on infinite streams given to `Enum.slice/2`
* [Kernel] Raise error on functions/guards without implementation
* [Keyword] Ensure keyword replace and update preserve order
* [Module] Raise instead of silently failing when performing a write module operation during after-compile
* [Module] Fix `@macrocallback` definitions with a `when` clause
* [Stream] Close with correct accumulator in `Stream.resource/3` when called for a single-element list
* [Stream] Allow `Stream.cycle/1` to be double nested inside `Stream.cycle/1`
* [URI] Preserve slashes in URIs without authority
* [Application] Warn if non-atom keys are given to `put_env`, `get_env`, `fetch_env`, and `delete_env`
* [Code] Do not send language keyword through the `:static_atoms_encoder` in `Code.string_to_quoted`
* [Kernel] Validate values given to `:line` in quote to avoid emitting invalid ASTs
* [Kernel] Report the correct line number when raising inside a macro
* [Kernel] Fix an issue where `elixirc` would not accept paths with backslash (`\`) separators on Windows
* [Kernel] Properly parse `&//2` (i.e. the capture of the division operator)
* [Kernel] Raise `CompileError` when trying to define reserved types
* [Kernel] Improve compiler error message when using `|` in a `def` signature
* [Kernel] Improve error message when trying to use invalid list operators in guards
* [Kernel.SpecialForms] Add `|/2` to the list of special forms to avoid inconsistent behaviour on overrides
* [Keyword] Enforce keys to be atoms in `Keyword.keys/1`
* [Record] Keep lexical ordering when creating records
* [Registry] Do not crash when a process with key-value has been registered using `:via` and it fails to start on `init`
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to [URL's living standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded) - note this behaviour is not specified in the spec implemented by the URI module, so the living standard was chosen
* [Version] Add defaults and enforce keys in `Version` struct
#### ExUnit
* [ExUnit.CaptureIO] Fix race condition where a dead capture would still be considered as active
* [ExUnit.Diff] Do not crash when failing to eval/inspect struct
* [ExUnit.Diff] Properly diff numbers in respect to `==` and `===` operators
#### IEx
* [IEx] Exit IEx session if the group leader exits
* [IEx] Allow `pry` to be used in non-tty terminals
* [IEx] Fix tokenizer emitting repeated warnings in the REPL
* [IEx] Ensure `--dot-iex` is preserved when restarting the evaluator and after shell respawn
* [IEx.Pry] Ensure `IEx.pry` can be triggered more than twice when invoked from the same process
#### Mix
* [mix compile] Do not filter out warning for external files from diagnostics
* [Mix.Project] Ensure user given `:manager` to dependencies has higher precedence than the SCM one
* [Mix.Project] Recompile umbrella children when config files change and `mix compile` is called from the umbrella root
* [Mix.Task] Always recompile before running tasks from dependencies
* [Mix.Task] Ensure project's Logger config is used when running Mix tasks
* [mix cmd] Fix a bug where only the first --app option would be executed
* [mix compile] Fix an issue where new protocol implementations would not propagate when running `mix compile` from an umbrella root
* [mix deps.compile] Use `gmake` instead of `make` when compiling deps on NetBSD/DragonFlyBSD
* [mix release] Load `.app` from dependencies path when it is a project dependency
* [mix release] Always include "rel/overlays" in the list of overlays directories if available
* [mix release] Change `erts/bin/erl` binary mode to `0o755`
* [mix test] Compare to test coverage threshold inclusively
#### Logger
* [Logger] Print metadata for all types that implement String.Chars
### 3. Soft-deprecations (no warnings emitted)
#### Elixir
### Elixir
* [Code] `compiler_options/0` is deprecated in favor of `compiler_option/1`
* [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1`
* [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2`
#### Mix
### Logger
* [mix xref] `calls/0` is deprecated in favor of compiler tracer
* [mix xref] The `xref.exclude` option has been moved to `elixirc_options.no_warn_undefined` as the `xref` pass has been moved into the compiler
* [Logger] `warn` log level is deprecated in favor of `warning`
### Mix
* [mix release] `config/releases.exs` is deprecated in favor of a more general purpose `config/runtime.exs`
### 4. Hard-deprecations
#### Elixir
* [Code] `Code.load_file/2` has been deprecated in favor of `Code.require_file/2` or `Code.compile_file/2`
* [Code] `Code.loaded_files/0` and `Code.unload_file/1` have been deprecated in favor of `Code.required_files/0` and `Code.unrequire_file/1` respectively
* [Code] `Code.ensure_compiled?/1` is deprecated in favor of `Code.ensure_compiled/1`
* [String] `String.normalize/2` has been deprecated in favor of `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` which ship as part of Erlang/OTP 20+
* [Supervisor] `Supervisor.Spec.supervise/2` has been deprecated in favor of the new Supervisor child specification
* [Supervisor] The `:simple_one_for_one` strategy in `Supervisor` has been deprecated in favor of `DynamicSupervisor`
#### Logger
* [Logger] `:compile_time_purge_level` application environment configuration has been deprecated in favor of the more general `:compile_time_purge_matching` config
* [Logger] Deprecate logging non-chardata values
* [Supervisor] Deprecate `Supervisor.start_child/2` and `Supervisor.terminate_child/2` in favor of `DynamicSupervisor`
* [Supervisor.Spec] Deprecate `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` in favor of the new typespecs
* [System] Deprecate `System.stacktrace/0` in favor of `__STACKTRACE__`
#### Mix
* [mix compile.xref] This check has been moved into the compiler and has no effect now
* [mix xref deprecations] This check has been moved into the compiler and has no effect now
* [mix xref unreachable] This check has been moved into the compiler and has no effect now
* [Mix.Project] Deprecate `Mix.Project.compile/2` in favor of `Mix.Task.run("compile", args)`
## v1.9
## v1.10
The CHANGELOG for v1.9 releases can be found [in the v1.9 branch](https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md).
The CHANGELOG for v1.10 releases can be found [in the v1.10 branch](https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md).
+12 -11
View File
@@ -2,9 +2,10 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := master/ # master/ or vMAJOR.MINOR/
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include $(ERLC_OPTS)
CANONICAL := v1.11/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
@@ -74,7 +75,7 @@ compile: erlang $(APP) elixir
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && erl -make
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -145,12 +146,12 @@ check_reproducible: compile
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) $(MAKE) compile
$(Q) echo "Diffing..."
$(Q) diff -r lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
$(Q) diff -r lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
$(Q) diff -r lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
$(Q) diff -r lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
$(Q) diff -r lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
$(Q) diff -r lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
$(Q) bin/elixir lib/elixir/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible"
clean:
@@ -294,7 +295,7 @@ PLT = .elixir.plt
$(PLT):
@ echo "==> Building PLT with Elixir's dependencies..."
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets crypto runtime_tools ftp tftp mnesia public_key asn1 hipe sasl
clean_plt:
$(Q) rm -f $(PLT)
+3 -3
View File
@@ -1,6 +1,6 @@
![Elixir](https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png)
=========
[![Build status](https://api.cirrus-ci.com/github/elixir-lang/elixir.svg?branch=master)](https://cirrus-ci.com/github/elixir-lang/elixir)
# ![Elixir](https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png)
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=master)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amaster+workflow%3ACI) [![Build status](https://api.cirrus-ci.com/github/elixir-lang/elixir.svg?branch=master)](https://cirrus-ci.com/github/elixir-lang/elixir)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
+3 -4
View File
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
| Elixir version | Support
| -------------- | ------------------------------
| 1.10 | Development
| 1.9 | Bug fixes and security patches
| 1.11 | Bug fixes and security patches
| 1.10 | Security patches only
| 1.9 | Security patches only
| 1.8 | Security patches only
| 1.7 | Security patches only
| 1.6 | Security patches only
| 1.5 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.10.0-dev
1.11.1
+4
View File
@@ -152,6 +152,10 @@ for /d %%d in ("!SCRIPT_PATH!..\lib\*.") do (
)
:run
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
)
if not !runMode! == "iex" (
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
)
+13 -11
View File
@@ -1,9 +1,9 @@
defmodule EEx.SyntaxError do
defexception [:message, :file, :line]
defexception [:message, :file, :line, :column]
@impl true
def message(exception) do
"#{exception.file}:#{exception.line}: #{exception.message}"
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
end
end
@@ -19,17 +19,17 @@ defmodule EEx do
This module provides 3 main APIs for you to use:
1. Evaluate a string (`eval_string`) or a file (`eval_file`)
1. Evaluate a string (`eval_string/3`) or a file (`eval_file/3`)
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
2. Define a function from a string (`function_from_string/5`)
or a file (`function_from_file/5`). 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`)
3. Compile a string (`compile_string/2`) or a file (`compile_file/2`)
into Elixir syntax tree. This is the API used by both functions
above and is available to you if you want to provide your own
ways of handling the compiled template.
@@ -39,12 +39,14 @@ defmodule EEx do
All functions in this module accept EEx-related options.
They are:
* `:line` - the line to be used as the template start. Defaults to 1.
* `:file` - the file to be used in the template. Defaults to the given
file the template is read from or to "nofile" when compiling from a string.
* `:line` - the line to be used as the template start. Defaults to 1.
* `:indentation` - (since v1.11.0) an integer added to the column after every
new line. Defaults to 0.
* `:engine` - the EEx engine to be used for compilation.
* `:trim` - trims whitespace left/right of quotation tags. If a quotation
tag appears on its own in a given line, line endings are also removed.
* `:trim` - if true, trims whitespace left/right of quotation tags up until
newlines. At least one newline is retained. Defaults to false.
## Engine
@@ -67,7 +69,7 @@ defmodule EEx do
**must** use the equals sign (`=`). Since everything in
Elixir is an expression, there are no exceptions for this rule.
For example, while some template languages would special-case
`if/2` clauses, they are treated the same in EEx and
`if` clauses, they are treated the same in EEx and
also require `=` in order to have their result printed:
<%= if true do %>
@@ -82,7 +84,7 @@ defmodule EEx do
will be rendered as `<%= x + 3 %>`.
Notice that different engines may have different rules
Note that different engines may have different rules
for each tag. Other tags may be added in future versions.
### Macros
+57 -25
View File
@@ -13,23 +13,28 @@ defmodule EEx.Compiler do
def compile(source, opts) when is_binary(source) and is_list(opts) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
column = 1
indentation = opts[:indentation] || 0
trim = opts[:trim] || false
tokenizer_options = %{trim: trim, indentation: indentation}
case EEx.Tokenizer.tokenize(source, line, trim: trim) do
case EEx.Tokenizer.tokenize(source, line, column, tokenizer_options) do
{:ok, tokens} ->
state = %{
engine: opts[:engine] || @default_engine,
file: file,
line: line,
quoted: [],
start_line: nil
start_line: nil,
start_column: nil,
parser_options: Code.get_compiler_option(:parser_options)
}
init = state.engine.init(opts)
generate_buffer(tokens, init, [], state)
{:error, line, message} ->
raise EEx.SyntaxError, line: line, file: file, message: message
{:error, line, column, message} ->
raise EEx.SyntaxError, file: file, line: line, column: column, message: message
end
end
@@ -41,13 +46,19 @@ defmodule EEx.Compiler do
generate_buffer(rest, buffer, scope, state)
end
defp generate_buffer([{:expr, line, mark, chars, _} | rest], buffer, scope, state) do
expr = Code.string_to_quoted!(chars, line: line, file: state.file)
defp generate_buffer([{:expr, line, column, mark, chars} | rest], buffer, scope, state) do
options = [file: state.file, line: line, column: column(column, mark)] ++ state.parser_options
expr = Code.string_to_quoted!(chars, options)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
generate_buffer(rest, buffer, scope, state)
end
defp generate_buffer([{:start_expr, start_line, mark, chars, _} | rest], buffer, scope, state) do
defp generate_buffer(
[{:start_expr, start_line, start_column, mark, chars} | rest],
buffer,
scope,
state
) do
{contents, line, rest} = look_ahead_middle(rest, start_line, chars)
{contents, rest} =
@@ -55,7 +66,13 @@ defmodule EEx.Compiler do
rest,
state.engine.handle_begin(buffer),
[contents | scope],
%{state | quoted: [], line: line, start_line: start_line}
%{
state
| quoted: [],
line: line,
start_line: start_line,
start_column: column(start_column, mark)
}
)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
@@ -63,7 +80,7 @@ defmodule EEx.Compiler do
end
defp generate_buffer(
[{:middle_expr, line, '', chars, _} | rest],
[{:middle_expr, line, _column, '', chars} | rest],
buffer,
[current | scope],
state
@@ -74,7 +91,7 @@ defmodule EEx.Compiler do
end
defp generate_buffer(
[{:middle_expr, line, modifier, chars, trimmed?} | t],
[{:middle_expr, line, column, modifier, chars} | t],
buffer,
[_ | _] = scope,
state
@@ -84,27 +101,35 @@ defmodule EEx.Compiler do
"please remove \"#{modifier}\" accordingly"
:elixir_errors.erl_warn(line, state.file, message)
generate_buffer([{:middle_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
generate_buffer([{:middle_expr, line, column, '', chars} | t], buffer, scope, state)
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
# raise EEx.SyntaxError, message: message, file: state.file, line: line
end
defp generate_buffer([{:middle_expr, line, _, chars, _} | _], _buffer, [], state) do
defp generate_buffer([{:middle_expr, line, column, _, chars} | _], _buffer, [], state) do
raise EEx.SyntaxError,
message: "unexpected middle of expression <%#{chars}%>",
file: state.file,
line: line
line: line,
column: column
end
defp generate_buffer([{:end_expr, line, '', chars, _} | rest], buffer, [current | _], state) do
defp generate_buffer(
[{:end_expr, line, _column, '', chars} | rest],
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)
column = state.start_column
options = [file: state.file, line: state.start_line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(wrapped, options)
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
defp generate_buffer(
[{:end_expr, line, modifier, chars, trimmed?} | t],
[{:end_expr, line, column, modifier, chars} | t],
buffer,
[_ | _] = scope,
state
@@ -114,27 +139,29 @@ defmodule EEx.Compiler do
"expression \"<%#{modifier}#{chars}%>\", please remove \"#{modifier}\" accordingly"
:elixir_errors.erl_warn(line, state.file, message)
generate_buffer([{:end_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
generate_buffer([{:end_expr, line, column, '', chars} | t], buffer, scope, state)
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
# raise EEx.SyntaxError, message: message, file: state.file, line: line
# raise EEx.SyntaxError, message: message, file: state.file, line: line, column: column
end
defp generate_buffer([{:end_expr, line, _, chars, _} | _], _buffer, [], state) do
defp generate_buffer([{:end_expr, line, column, _, chars} | _], _buffer, [], state) do
raise EEx.SyntaxError,
message: "unexpected end of expression <%#{chars}%>",
file: state.file,
line: line
line: line,
column: column
end
defp generate_buffer([], buffer, [], state) do
defp generate_buffer([{:eof, _, _}], buffer, [], state) do
state.engine.handle_body(buffer)
end
defp generate_buffer([], _buffer, _scope, state) do
defp generate_buffer([{:eof, line, column}], _buffer, _scope, state) do
raise EEx.SyntaxError,
message: "unexpected end of string, expected a closing '<% end %>'",
file: state.file,
line: state.line
line: line,
column: column
end
# Creates a placeholder and wrap it inside the expression block
@@ -152,7 +179,7 @@ defmodule EEx.Compiler do
# Look middle expressions that immediately follow a start_expr
defp look_ahead_middle(
[{:text, text}, {:middle_expr, line, _, chars, _} | rest] = tokens,
[{:text, text}, {:middle_expr, line, _column, _, chars} | rest] = tokens,
start,
contents
) do
@@ -163,7 +190,7 @@ defmodule EEx.Compiler do
end
end
defp look_ahead_middle([{:middle_expr, line, _, chars, _} | rest], _start, contents) do
defp look_ahead_middle([{:middle_expr, line, _column, _, chars} | rest], _start, contents) do
{contents ++ chars, line, rest}
end
@@ -197,4 +224,9 @@ defmodule EEx.Compiler do
defp insert_quoted(other, _quoted) do
other
end
defp column(column, mark) do
# length('<%') == 2
column + 2 + length(mark)
end
end
+111 -145
View File
@@ -3,14 +3,14 @@ defmodule EEx.Tokenizer do
@type content :: IO.chardata()
@type line :: non_neg_integer
@type column :: non_neg_integer
@type marker :: '=' | '/' | '|' | ''
@type trimmed? :: boolean
@type token ::
{:text, content}
| {:expr | :start_expr | :middle_expr | :end_expr, line, marker, content, trimmed?}
| {:expr | :start_expr | :middle_expr | :end_expr, line, column, marker, content}
| {:eof, line, column}
@spaces [?\s, ?\t]
@closing_brackets ')]}'
@doc """
Tokenizes the given charlist or binary.
@@ -18,68 +18,79 @@ defmodule EEx.Tokenizer do
It returns {:ok, list} with the following tokens:
* `{:text, content}`
* `{:expr, line, marker, content, trimmed?}`
* `{:start_expr, line, marker, content, trimmed?}`
* `{:middle_expr, line, marker, content, trimmed?}`
* `{:end_expr, line, marker, content, trimmed?}`
* `{:expr, line, column, marker, content}`
* `{:start_expr, line, column, marker, content}`
* `{:middle_expr, line, column, marker, content}`
* `{:end_expr, line, column, marker, content}`
* `{:eof, line, column}`
Or `{:error, line, error}` in case of errors.
Or `{:error, line, column, message}` in case of errors.
"""
@spec tokenize(binary | charlist, line, keyword) :: {:ok, [token]} | {:error, line, String.t()}
def tokenize(bin, line, opts \\ [])
@spec tokenize(binary | charlist, line, column, map) ::
{:ok, [token]} | {:error, line, column, String.t()}
def tokenize(bin, line, opts)
when is_binary(bin) and is_integer(line) and line >= 0 and is_list(opts) do
tokenize(String.to_charlist(bin), line, opts)
def tokenize(bin, line, column, opts) when is_binary(bin) do
tokenize(String.to_charlist(bin), line, column, opts)
end
def tokenize(list, line, opts)
when is_list(list) and is_integer(line) and line >= 0 and is_list(opts) do
tokenize(list, line, opts, [], [])
def tokenize(list, line, column, opts)
when is_list(list) and is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
column = opts.indentation + column
{list, line, column} = (opts.trim && trim_init(list, line, column)) || {list, line, column}
tokenize(list, line, column, opts, [], [])
end
defp tokenize('<%%' ++ t, line, opts, buffer, acc) do
tokenize(t, line, opts, [?%, ?< | buffer], acc)
defp tokenize('<%%' ++ t, line, column, opts, buffer, acc) do
tokenize(t, line, column + 3, opts, [?%, ?< | buffer], acc)
end
defp tokenize('<%#' ++ t, line, opts, buffer, acc) do
case expr(t, line, []) do
{:error, _, _} = error ->
defp tokenize('<%#' ++ t, line, column, opts, buffer, acc) do
case expr(t, line, column + 3, opts, []) do
{:error, _, _, _} = error ->
error
{:ok, _, new_line, rest} ->
{_, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
tokenize(rest, new_line, opts, buffer, acc)
{:ok, _, new_line, new_column, rest} ->
{rest, new_line, new_column, buffer} =
trim_if_needed(rest, new_line, new_column, opts, buffer)
tokenize(rest, new_line, new_column, opts, buffer, acc)
end
end
defp tokenize('<%' ++ t, line, opts, buffer, acc) do
defp tokenize('<%' ++ t, line, column, opts, buffer, acc) do
{marker, t} = retrieve_marker(t)
case expr(t, line, []) do
{:error, _, _} = error ->
case expr(t, line, column + 2 + length(marker), opts, []) do
{:error, _, _, _} = error ->
error
{:ok, expr, new_line, rest} ->
token = token_name(expr)
{trimmed?, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
expr = pad_if_needed(token, expr, trimmed?)
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, tokens} -> token_key(tokens, expr)
{:error, _, _, _} -> {:expr, expr}
end
{rest, new_line, new_column, buffer} =
trim_if_needed(rest, new_line, new_column, opts, buffer)
acc = tokenize_text(buffer, acc)
final = {token, line, marker, Enum.reverse(expr), trimmed?}
tokenize(rest, new_line, opts, [], [final | acc])
final = {key, line, column, marker, expr}
tokenize(rest, new_line, new_column, opts, [], [final | acc])
end
end
defp tokenize('\n' ++ t, line, opts, buffer, acc) do
tokenize(t, line + 1, opts, [?\n | buffer], acc)
defp tokenize('\n' ++ t, line, _column, opts, buffer, acc) do
tokenize(t, line + 1, opts.indentation + 1, opts, [?\n | buffer], acc)
end
defp tokenize([h | t], line, opts, buffer, acc) do
tokenize(t, line, opts, [h | buffer], acc)
defp tokenize([h | t], line, column, opts, buffer, acc) do
tokenize(t, line, column + 1, opts, [h | buffer], acc)
end
defp tokenize([], _line, _opts, buffer, acc) do
{:ok, Enum.reverse(tokenize_text(buffer, acc))}
defp tokenize([], line, column, _opts, buffer, acc) do
eof = {:eof, line, column}
{:ok, Enum.reverse([eof | tokenize_text(buffer, acc)])}
end
# Retrieve marker for <%
@@ -94,102 +105,61 @@ defmodule EEx.Tokenizer do
# Tokenize an expression until we find %>
defp expr([?%, ?> | t], line, buffer) do
{:ok, buffer, line, t}
defp expr([?%, ?> | t], line, column, _opts, buffer) do
{:ok, Enum.reverse(buffer), line, column + 2, t}
end
defp expr('\n' ++ t, line, buffer) do
expr(t, line + 1, [?\n | buffer])
defp expr('\n' ++ t, line, _column, opts, buffer) do
expr(t, line + 1, opts.indentation + 1, opts, [?\n | buffer])
end
defp expr([h | t], line, buffer) do
expr(t, line, [h | buffer])
defp expr([h | t], line, column, opts, buffer) do
expr(t, line, column + 1, opts, [h | buffer])
end
defp expr([], line, _buffer) do
{:error, line, "missing token '%>'"}
defp expr([], line, column, _opts, _buffer) do
{:error, line, column, "missing token '%>'"}
end
# Receive an expression content and check
# if it is a start, middle or an end token.
#
# Start tokens finish with "do" and "fn ->"
# Middle tokens are marked with "->" or keywords
# End tokens contain only the end word and optionally
# combinations of ")", "]" and "}".
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(tokens, expr) do
case {tokens, Enum.reverse(tokens)} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
defp token_name([h | t]) when h in @spaces do
token_name(t)
end
{_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr)}
defp token_name('od' ++ [h | rest]) when h in @spaces or h in @closing_brackets do
case tokenize_rest(rest) do
{:ok, [{:end, _} | _]} -> :middle_expr
_ -> :start_expr
end
end
{_, [{:block_identifier, _, _} | _]} ->
{:middle_expr, maybe_append_space(expr)}
defp token_name('>-' ++ rest) do
case tokenize_rest(rest) do
{:ok, [{:end, _} | _]} ->
:middle_expr
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
{:middle_expr, expr}
# Check if there is a "fn" token and, if so, it is not
# followed by an "end" token. If this is the case, we
# are on a start expr.
{:ok, tokens} ->
tokens = Enum.reverse(tokens)
fn_index = fn_index(tokens)
{_, [{:stab_op, _, _} | reverse_tokens]} ->
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if fn_index && end_index(tokens) > fn_index do
:start_expr
if end_index > fn_index do
{:start_expr, expr}
else
:middle_expr
{:middle_expr, expr}
end
_error ->
:middle_expr
{tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr}
end
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 maybe_append_space([?\s]), do: [?\s]
defp maybe_append_space([h]), do: [h, ?\s]
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
defp token_name(rest) do
case Enum.drop_while(rest, &(&1 in @spaces or &1 in @closing_brackets)) do
'dne' ++ t -> check_spaces(t, :end_expr)
_ -> :expr
end
end
# Tokenize the remaining passing check_terminators as false,
# which relax the tokenizer to not error on unmatched pairs.
# If the tokens start with an "end" we have a middle expr.
defp tokenize_rest(rest) do
:elixir_tokenizer.tokenize(Enum.reverse(rest), 1, file: "eex", check_terminators: false)
end
defp fn_index(tokens) do
Enum.find_index(tokens, fn
{:fn_paren, _} -> true
{:fn, _} -> true
_ -> false
end)
end
defp end_index(tokens) do
Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
end
defp check_spaces(string, token) do
if Enum.all?(string, &(&1 in @spaces)) do
token
else
:expr
end
end
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
defp closing_bracket?(_), do: false
# Tokenize the buffered text by appending
# it to the given accumulator.
@@ -202,45 +172,41 @@ defmodule EEx.Tokenizer do
[{:text, Enum.reverse(buffer)} | acc]
end
# If trim mode is enabled and the token is on a line with
# only itself and whitespace, trim the whitespace around it,
# including the line break following it if there is one.
defp trim_if_needed(rest, line, opts, buffer, acc) do
with true <- opts[:trim],
{true, new_buffer} <- trim_left(buffer, acc),
{true, new_rest, new_line} <- trim_right(rest, line) do
{true, new_rest, new_line, new_buffer}
defp trim_if_needed(rest, line, column, opts, buffer) do
if opts.trim do
buffer = trim_left(buffer, 0)
{rest, line, column} = trim_right(rest, line, column, 0)
{rest, line, column, buffer}
else
_ -> {false, rest, line, buffer}
{rest, line, column, buffer}
end
end
defp trim_left(buffer, acc) do
case {trim_whitespace(buffer), acc} do
{[?\n | _] = trimmed_buffer, _} -> {true, trimmed_buffer}
{[], [{_, _, _, _, true} | _]} -> {true, []}
{[], []} -> {true, []}
_ -> {false, buffer}
defp trim_init([h | t], line, column) when h in @spaces, do: trim_init(t, line, column + 1)
defp trim_init([?\r, ?\n | t], line, _column), do: trim_init(t, line + 1, 1)
defp trim_init([?\n | t], line, _column), do: trim_init(t, line + 1, 1)
defp trim_init([?<, ?% | _] = rest, line, column), do: {rest, line, column}
defp trim_init(_, _, _), do: false
defp trim_left(buffer, count) do
case trim_whitespace(buffer) do
[?\n, ?\r | rest] -> trim_left(rest, count + 1)
[?\n | rest] -> trim_left(rest, count + 1)
_ when count > 0 -> [?\n | buffer]
_ -> buffer
end
end
defp trim_right(rest, line) do
defp trim_right(rest, line, column, count) do
case trim_whitespace(rest) do
[?\r, ?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
[?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
[] -> {true, [], line}
_ -> {false, rest, line}
[?\r, ?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
[?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
[] -> {[], line, column + length(rest)}
_ when count > 0 -> {[?\n | rest], line - 1, column}
_ -> {rest, line, column}
end
end
defp trim_whitespace([h | t]) when h in @spaces do
trim_whitespace(t)
end
defp trim_whitespace(list) do
list
end
defp pad_if_needed(:start_expr, [h | _] = expr, true) when h not in @spaces, do: [?\s | expr]
defp pad_if_needed(_, expr, _), do: expr
defp trim_whitespace([h | t]) when h in @spaces, do: trim_whitespace(t)
defp trim_whitespace(list), do: list
end
+119 -71
View File
@@ -4,37 +4,39 @@ defmodule EEx.TokenizerTest do
use ExUnit.Case, async: true
require EEx.Tokenizer, as: T
@opts %{indentation: 0, trim: false}
test "simple chars lists" do
assert T.tokenize('foo', 1) == {:ok, [{:text, 'foo'}]}
assert T.tokenize('foo', 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
end
test "simple strings" do
assert T.tokenize("foo", 1) == {:ok, [{:text, 'foo'}]}
assert T.tokenize("foo", 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
end
test "strings with embedded code" do
assert T.tokenize('foo <% bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '', ' bar ', false}]}
assert T.tokenize('foo <% bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '', ' bar '}, {:eof, 1, 14}]}
end
test "strings with embedded equals code" do
assert T.tokenize('foo <%= bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '=', ' bar ', false}]}
assert T.tokenize('foo <%= bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '=', ' bar '}, {:eof, 1, 15}]}
end
test "strings with embedded slash code" do
assert T.tokenize('foo <%/ bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '/', ' bar ', false}]}
assert T.tokenize('foo <%/ bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '/', ' bar '}, {:eof, 1, 15}]}
end
test "strings with embedded pipe code" do
assert T.tokenize('foo <%| bar %>', 1) ==
{:ok, [{:text, 'foo '}, {:expr, 1, '|', ' bar ', false}]}
assert T.tokenize('foo <%| bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '|', ' bar '}, {:eof, 1, 15}]}
end
test "strings with more than one line" do
assert T.tokenize('foo\n<%= bar %>', 1) ==
{:ok, [{:text, 'foo\n'}, {:expr, 2, '=', ' bar ', false}]}
assert T.tokenize('foo\n<%= bar %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo\n'}, {:expr, 2, 1, '=', ' bar '}, {:eof, 2, 11}]}
end
test "strings with more than one line and expression with more than one line" do
@@ -47,167 +49,208 @@ defmodule EEx.TokenizerTest do
exprs = [
{:text, 'foo '},
{:expr, 1, '=', ' bar\n\nbaz ', false},
{:expr, 1, 5, '=', ' bar\n\nbaz '},
{:text, '\n'},
{:expr, 4, '', ' foo ', false},
{:text, '\n'}
{:expr, 4, 1, '', ' foo '},
{:text, '\n'},
{:eof, 5, 1}
]
assert T.tokenize(string, 1) == {:ok, exprs}
assert T.tokenize(string, 1, 1, @opts) == {:ok, exprs}
end
test "quotation" do
assert T.tokenize('foo <%% true %>', 1) == {:ok, [{:text, 'foo <% true %>'}]}
assert T.tokenize('foo <%% true %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo <% true %>'}, {:eof, 1, 16}]}
end
test "quotation with do/end" do
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1) ==
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}]}
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1, 1, @opts) ==
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}, {:eof, 1, 32}]}
end
test "quotation with interpolation" do
exprs = [
{:text, 'a <% b '},
{:expr, 1, '=', ' c ', false},
{:expr, 1, 9, '=', ' c '},
{:text, ' '},
{:expr, 1, '=', ' d ', false},
{:text, ' e %> f'}
{:expr, 1, 18, '=', ' d '},
{:text, ' e %> f'},
{:eof, 1, 33}
]
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1) == {:ok, exprs}
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1, 1, @opts) == {:ok, exprs}
end
test "improperly formatted quotation with interpolation" do
exprs = [
{:text, '<%% a <%= b %> c %>'}
{:text, '<%% a <%= b %> c %>'},
{:eof, 1, 22}
]
assert T.tokenize('<%%% a <%%= b %> c %>', 1) == {:ok, exprs}
assert T.tokenize('<%%% a <%%= b %> c %>', 1, 1, @opts) == {:ok, exprs}
end
test "comments" do
test "eex comments" do
exprs = [
{:text, 'foo '}
{:text, 'foo '},
{:eof, 1, 16}
]
assert T.tokenize('foo <%# true %>', 1) == {:ok, exprs}
assert T.tokenize('foo <%# true %>', 1, 1, @opts) == {:ok, exprs}
end
test "comments with do/end" do
test "eex comments with do/end" do
exprs = [
{:text, 'foo bar'}
{:text, 'foo bar'},
{:eof, 1, 32}
]
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == {:ok, exprs}
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1, 1, @opts) == {:ok, exprs}
end
test "elixir comments" do
exprs = [
{:text, 'foo '},
{:expr, 1, 5, [], ' true # this is a boolean '},
{:eof, 1, 35}
]
assert T.tokenize('foo <% true # this is a boolean %>', 1, 1, @opts) == {:ok, exprs}
end
test "elixir comments with do/end" do
exprs = [
{:start_expr, 1, 1, [], ' if true do # startif '},
{:text, 'text'},
{:end_expr, 1, 31, [], ' end # closeif '},
{:eof, 1, 50}
]
assert T.tokenize('<% if true do # startif %>text<% end # closeif %>', 1, 1, @opts) ==
{:ok, exprs}
end
test "strings with embedded do end" do
exprs = [
{:text, 'foo '},
{:start_expr, 1, '', ' if true do ', false},
{:start_expr, 1, 5, '', ' if true do '},
{:text, 'bar'},
{:end_expr, 1, '', ' end ', false}
{:end_expr, 1, 24, '', ' end '},
{:eof, 1, 33}
]
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == {:ok, exprs}
assert T.tokenize('foo <% if true do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
end
test "strings with embedded -> end" do
exprs = [
{:text, 'foo '},
{:start_expr, 1, '', ' cond do ', false},
{:middle_expr, 1, '', ' false -> ', false},
{:start_expr, 1, 5, '', ' cond do '},
{:middle_expr, 1, 18, '', ' false -> '},
{:text, 'bar'},
{:middle_expr, 1, '', ' true -> ', false},
{:middle_expr, 1, 35, '', ' true -> '},
{:text, 'baz'},
{:end_expr, 1, '', ' end ', false}
{:end_expr, 1, 51, '', ' end '},
{:eof, 1, 60}
]
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) ==
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1, 1, @opts) ==
{:ok, exprs}
end
test "strings with multiple callbacks" do
exprs = [
{:start_expr, 1, '=', ' a fn -> ', false},
{:start_expr, 1, 1, '=', ' a fn -> '},
{:text, 'foo'},
{:middle_expr, 1, '', ' end, fn -> ', false},
{:middle_expr, 1, 18, '', ' end, fn -> '},
{:text, 'bar'},
{:end_expr, 1, '', ' end ', false}
{:end_expr, 1, 37, '', ' end '},
{:eof, 1, 46}
]
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1) == {:ok, exprs}
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1, 1, @opts) ==
{:ok, exprs}
end
test "strings with callback followed by do block" do
exprs = [
{:start_expr, 1, '=', ' a fn -> ', false},
{:start_expr, 1, 1, '=', ' a fn -> '},
{:text, 'foo'},
{:middle_expr, 1, '', ' end do ', false},
{:middle_expr, 1, 18, '', ' end do '},
{:text, 'bar'},
{:end_expr, 1, '', ' end ', false}
{:end_expr, 1, 33, '', ' end '},
{:eof, 1, 42}
]
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1) == {:ok, exprs}
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
end
test "strings with embedded keywords blocks" do
exprs = [
{:text, 'foo '},
{:start_expr, 1, '', ' if true do ', false},
{:start_expr, 1, 5, '', ' if true do '},
{:text, 'bar'},
{:middle_expr, 1, '', ' else ', false},
{:middle_expr, 1, 24, '', ' else '},
{:text, 'baz'},
{:end_expr, 1, '', ' end ', false}
{:end_expr, 1, 37, '', ' end '},
{:eof, 1, 46}
]
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == {:ok, exprs}
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1, 1, @opts) ==
{:ok, exprs}
end
test "trim mode" do
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> '
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> \n\n '
exprs = [
{:start_expr, 1, '=', ' if true do ', true},
{:text, ' TRUE \n'},
{:middle_expr, 3, '', ' else ', true},
{:text, ' FALSE \n'},
{:end_expr, 5, '', ' end ', true}
{:start_expr, 1, 2, '=', ' if true do '},
{:text, '\n TRUE \n'},
{:middle_expr, 3, 3, '', ' else '},
{:text, '\n FALSE \n'},
{:end_expr, 5, 3, '', ' end '},
{:eof, 7, 3}
]
assert T.tokenize(template, 1, trim: true) == {:ok, exprs}
assert T.tokenize(template, 1, 1, %{@opts | trim: true}) == {:ok, exprs}
end
test "trim mode with comment" do
exprs = [
{:text, '123'}
{:text, '\n123'},
{:eof, 2, 4}
]
assert T.tokenize(' <%# comment %> \n123', 1, trim: true) == {:ok, exprs}
assert T.tokenize(' <%# comment %> \n123', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
end
test "trim mode with CRLF" do
exprs = [
{:text, '0\r\n'},
{:expr, 2, '=', ' 12 ', true},
{:text, '34'}
{:text, '0\n'},
{:expr, 2, 3, '=', ' 12 '},
{:text, '\n34'},
{:eof, 3, 3}
]
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, trim: true) == {:ok, exprs}
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
end
test "trim mode set to false" do
exprs = [
{:text, ' '},
{:expr, 1, '=', ' 12 ', false},
{:text, ' \n'}
{:expr, 1, 2, '=', ' 12 '},
{:text, ' \n'},
{:eof, 2, 1}
]
assert T.tokenize(' <%= 12 %> \n', 1, trim: false) == {:ok, exprs}
assert T.tokenize(' <%= 12 %> \n', 1, 1, %{@opts | trim: false}) == {:ok, exprs}
end
test "trim mode no false positives" do
assert_not_trimmed = fn x -> assert T.tokenize(x, 1, trim: true) == T.tokenize(x, 1) end
assert_not_trimmed = fn x ->
assert T.tokenize(x, 1, 1, %{@opts | trim: false}) == T.tokenize(x, 1, 1, @opts)
end
assert_not_trimmed.('foo <%= "bar" %> ')
assert_not_trimmed.('\n <%= "foo" %>bar')
@@ -215,8 +258,13 @@ defmodule EEx.TokenizerTest do
assert_not_trimmed.(' <%= 01 %><%= 23 %>\n')
end
test "raise syntax error when there is start mark and no end mark" do
assert T.tokenize('foo <% :bar', 1) == {:error, 1, "missing token '%>'"}
assert T.tokenize('<%# true ', 1) == {:error, 1, "missing token '%>'"}
test "returns error when there is start mark and no end mark" do
assert T.tokenize('foo <% :bar', 1, 1, @opts) == {:error, 1, 12, "missing token '%>'"}
assert T.tokenize('<%# true ', 1, 1, @opts) == {:error, 1, 10, "missing token '%>'"}
end
test "marks invalid expressions as regular expressions" do
assert T.tokenize('<% 1 $ 2 %>', 1, 1, @opts) ==
{:ok, [{:expr, 1, 1, [], ' 1 $ 2 '}, {:eof, 1, 12}]}
end
end
+77 -13
View File
@@ -79,8 +79,24 @@ defmodule EExTest do
end
test "trim mode" do
string = "<%= 123 %> \n \n <%= 789 %>"
expected = "123\n789"
assert_eval(expected, string, [], trim: true)
string = "<%= 123 %> \n456\n <%= 789 %>"
expected = "123456\n789"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
string = "<%= 123 %> \n <%= 456 %> \n <%= 789 %>"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
string = "\n <%= 123 %> \n <%= 456 %> \n <%= 789 %> \n"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
string = "\r\n <%= 123 %> \r\n <%= 456 %> \r\n <%= 789 %> \r\n"
expected = "123\n456\n789"
assert_eval(expected, string, [], trim: true)
end
@@ -94,7 +110,7 @@ defmodule EExTest do
<% end %>
"""
expected = " that\n"
expected = "\n that\n"
assert_eval(expected, string, [], trim: true)
end
@@ -106,11 +122,22 @@ defmodule EExTest do
<%= "Fourth line" %>
"""
expected = "First lineSecond lineThird lineFourth line"
expected = "First line\nSecond line\nThird line\nFourth line"
assert_eval(expected, string, [], trim: true)
end
test "trim mode with no spaces" do
string = """
<%=if true do%>
this
<%else%>
that
<%end%>
"""
expected = "\n this\n"
assert_eval(expected, string, [], trim: true)
string = """
<%=cond do%>
<%false ->%>
@@ -120,7 +147,7 @@ defmodule EExTest do
<%end%>
"""
expected = " that\n"
expected = "\n that\n"
assert_eval(expected, string, [], trim: true)
end
@@ -220,25 +247,27 @@ defmodule EExTest do
describe "raises syntax errors" do
test "when the token is invalid" do
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
assert_raise EEx.SyntaxError, "nofile:1:12: missing token '%>'", fn ->
EEx.compile_string("foo <%= bar")
end
end
test "when middle expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected middle of expression <% else %>", fn ->
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
end
assert_raise EEx.SyntaxError,
"nofile:1:18: unexpected middle of expression <% else %>",
fn ->
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
end
end
test "when end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
assert_raise EEx.SyntaxError, "nofile:1:5: unexpected end of expression <% end %>", fn ->
EEx.compile_string("foo <% end %>")
end
end
test "when start expression is found without an end expression" do
msg = "nofile:2: unexpected end of string, expected a closing '<% end %>'"
msg = "nofile:2:17: unexpected end of string, expected a closing '<% end %>'"
assert_raise EEx.SyntaxError, msg, fn ->
EEx.compile_string("foo\n<% if true do %>")
@@ -246,7 +275,7 @@ defmodule EExTest do
end
test "when nested end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
assert_raise EEx.SyntaxError, "nofile:1:30: unexpected end of expression <% end %>", fn ->
EEx.compile_string("foo <% if true do %><% end %><% end %>")
end
end
@@ -285,13 +314,13 @@ defmodule EExTest do
describe "error messages" do
test "honor line numbers" do
assert_raise EEx.SyntaxError, "nofile:99: missing token '%>'", fn ->
assert_raise EEx.SyntaxError, "nofile:99:12: missing token '%>'", fn ->
EEx.compile_string("foo <%= bar", line: 99)
end
end
test "honor file names" do
assert_raise EEx.SyntaxError, "my_file.eex:1: missing token '%>'", fn ->
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
EEx.compile_string("foo <%= bar", file: "my_file.eex")
end
end
@@ -502,6 +531,41 @@ defmodule EExTest do
assert_eval("\n\n Good\n \n", string)
end
test "line and column meta" do
parser_options = Code.get_compiler_option(:parser_options)
Code.put_compiler_option(:parser_options, columns: true)
try do
indentation = 12
ast =
EEx.compile_string(
"""
<%= f() %> <% f() %>
<%= f fn -> %>
<%= f() %>
<% end %>
""",
indentation: indentation
)
{_, calls} =
Macro.prewalk(ast, [], fn
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
other, acc -> {other, acc}
end)
assert Enum.reverse(calls) == [
[line: 1, column: indentation + 5],
[line: 1, column: indentation + 15],
[line: 2, column: indentation + 7],
[line: 3, column: indentation + 9]
]
after
Code.put_compiler_option(:parser_options, parser_options)
end
end
end
describe "buffers" do
-1
View File
@@ -11,7 +11,6 @@
warn_exported_vars,
%% warn_missing_spec,
%% warn_untyped_record,
warnings_as_errors,
debug_info,
{outdir, "ebin/"}
]}.
+156
View File
@@ -0,0 +1,156 @@
defmodule Diff do
@moduledoc """
Utilities for comparing build artifacts.
"""
@known_chunks ~w(
abstract_code
debug_info
attributes
compile_info
exports
labeled_exports
imports
indexed_imports
locals
labeled_locals
atoms
)a
@doc """
Compares the build artifacts of two build directories.
"""
@spec compare_dirs(Path.t(), Path.t()) ::
{
only1_paths :: list(Path.t()),
only2_paths :: list(Path.t()),
diff :: list({Path.t(), diff :: String.t()})
}
def compare_dirs(dir1, dir2) do
dir1 = Path.expand(dir1)
dir2 = Path.expand(dir2)
assert_dir!(dir1)
assert_dir!(dir2)
dir1_paths = relative_paths(dir1)
dir2_paths = relative_paths(dir2)
only1_paths = dir1_paths -- dir2_paths
only2_paths = dir2_paths -- dir1_paths
common_paths = dir1_paths -- only1_paths
common_files = Enum.reject(common_paths, &File.dir?/1)
diff =
Enum.flat_map(common_files, fn path ->
file1 = Path.join(dir1, path)
file2 = Path.join(dir2, path)
case compare_files(file1, file2) do
:eq -> []
{:diff, diff} -> [{path, diff}]
end
end)
{only1_paths, only2_paths, diff}
end
@doc """
Compares the contents of two files.
If the files are BEAM files, it performs a more human-friendly
"BEAM-diff".
"""
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
def compare_files(file1, file2) do
content1 = File.read!(file1)
content2 = File.read!(file2)
if content1 == content2 do
:eq
else
diff =
if String.ends_with?(file1, ".beam") do
beam_diff(file1, content1, file2, content2)
else
file_diff(file1, file2)
end
{:diff, diff}
end
end
defp beam_diff(file1, content1, file2, content2) do
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks),
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks),
true <- chunks1 != chunks2 do
for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do
tmp_file1 =
chunk1
|> inspect(pretty: true, limit: :infinity)
|> write_tmp()
tmp_file2 =
chunk2
|> inspect(pretty: true, limit: :infinity)
|> write_tmp()
file_diff(tmp_file1, tmp_file2)
end
else
_ ->
file_diff(file1, file2)
end
end
defp file_diff(file1, file2) do
{diff, _} = System.cmd("diff", [file1, file2])
diff
end
defp relative_paths(dir) do
dir
|> Path.join("**")
|> Path.wildcard()
|> Enum.map(&Path.relative_to(&1, dir))
end
defp assert_dir!(dir) do
unless File.dir?(dir) do
raise ArgumentError, "#{inspect(dir)} is not a directory"
end
end
defp write_tmp(content) do
filename = generate_tmp_filename()
File.mkdir_p!("tmp")
File.write!(Path.join("tmp", filename), content)
Path.join("tmp", filename)
end
defp generate_tmp_filename do
sec = :os.system_time(:second)
rand = :rand.uniform(999_999_999)
scheduler_id = :erlang.system_info(:scheduler_id)
"tmp-#{sec}-#{rand}-#{scheduler_id}"
end
end
case System.argv() do
[dir1, dir2] ->
case Diff.compare_dirs(dir1, dir2) do
{[], [], []} ->
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
{only1, only2, diff} ->
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
System.halt(1)
end
_ ->
IO.puts("Please, provide two directories as arguments")
System.halt(1)
end
+14 -11
View File
@@ -1,10 +1,10 @@
# Returns config for Elixir docs
[
extras: Path.wildcard("lib/elixir/pages/*.md"),
extras: Path.wildcard("lib/elixir/pages/*.md") ++ ["CHANGELOG.md"],
groups_for_functions: [
Guards: & &1[:guard] == true
],
skip_undefined_reference_warnings_on: ["compatibility-and-deprecations"],
skip_undefined_reference_warnings_on: ["lib/elixir/pages/compatibility-and-deprecations.md"],
groups_for_modules: [
# [Kernel, Kernel.SpecialForms],
@@ -89,15 +89,18 @@
Kernel.ParallelCompiler,
Macro,
Macro.Env
],
Deprecated: [
Behaviour,
Dict,
GenEvent,
HashDict,
HashSet,
Set,
Supervisor.Spec
]
## Automatically detected groups
# Deprecated: [
# Behaviour,
# Dict,
# GenEvent,
# HashDict,
# HashSet,
# Set,
# Supervisor.Spec
# ]
]
]
+43 -11
View File
@@ -6,8 +6,8 @@ defmodule Access do
keys of any type in a data structure via the `data[key]` syntax.
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
of the box. The key can be of any type and it returns `nil` if
the key does not exist:
of the box. Keywords supports only atoms keys, keys for maps can
be of any type. Both returns `nil` if the key does not exist:
iex> keywords = [a: 1, b: 2]
iex> keywords[:a]
@@ -683,36 +683,68 @@ defmodule Access do
end
defp at(:get_and_update, data, index, next) when is_list(data) do
get_and_update_at(data, index, next, [])
get_and_update_at(data, index, next, [], fn -> nil end)
end
defp at(_op, data, _index, _next) do
raise "Access.at/1 expected a list, got: #{inspect(data)}"
end
defp get_and_update_at([head | rest], 0, next, updates) do
defp get_and_update_at([head | rest], 0, next, updates, _default_fun) do
case next.(head) do
{get, update} -> {get, :lists.reverse([update | updates], rest)}
:pop -> {head, :lists.reverse(updates, rest)}
end
end
defp get_and_update_at(list, index, next, updates) when index < 0 do
defp get_and_update_at([_ | _] = list, index, next, updates, default_fun) when index < 0 do
list_length = length(list)
if list_length + index >= 0 do
get_and_update_at(list, list_length + index, next, updates)
get_and_update_at(list, list_length + index, next, updates, default_fun)
else
{nil, list}
{default_fun.(), list}
end
end
defp get_and_update_at([head | rest], index, next, updates) when index > 0 do
get_and_update_at(rest, index - 1, next, [head | updates])
defp get_and_update_at([head | rest], index, next, updates, default_fun) when index > 0 do
get_and_update_at(rest, index - 1, next, [head | updates], default_fun)
end
defp get_and_update_at([], _index, _next, updates) do
{nil, :lists.reverse(updates)}
defp get_and_update_at([], _index, _next, updates, default_fun) do
{default_fun.(), :lists.reverse(updates)}
end
@doc ~S"""
Same as `at/1` except that it raises `Enum.OutOfBoundsError`
if the given index is out of bounds.
## Examples
iex> get_in([:a, :b, :c], [Access.at!(2)])
:c
iex> get_in([:a, :b, :c], [Access.at!(3)])
** (Enum.OutOfBoundsError) out of bounds error
"""
@spec at!(integer) :: access_fun(data :: list, get_value :: term)
def at!(index) when is_integer(index) do
fn op, data, next -> at!(op, data, index, next) end
end
defp at!(:get, data, index, next) when is_list(data) do
case Enum.fetch(data, index) do
{:ok, value} -> next.(value)
:error -> raise Enum.OutOfBoundsError
end
end
defp at!(:get_and_update, data, index, next) when is_list(data) do
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
end
defp at!(_op, data, _index, _next) do
raise "Access.at!/1 expected a list, got: #{inspect(data)}"
end
@doc ~S"""
+22 -11
View File
@@ -7,7 +7,7 @@ defmodule Application do
programming languages, but with some additional characteristics.
An application is a component implementing some specific functionality, with a
standardized directory structure, configuration, and lifecycle. Applications
standardized directory structure, configuration, and life cycle. Applications
are *loaded*, *started*, and *stopped*. Each application also has its own
environment, which provides a unified API for configuring each application.
@@ -84,8 +84,8 @@ defmodule Application do
This approach has one big limitation: if you change the value of the
application environment after the code is compiled, the value used at
runtime is not going to change! For example, if you are using `mix release`
and your `config/releases.exs` has:
runtime is not going to change! For example, if your `config/runtime.exs`
has:
config :my_app, :db_host, "db.production"
@@ -171,7 +171,7 @@ defmodule Application do
the documentation of `Mix.Tasks.Compile.App`, available as well by running
`mix help compile.app`.
## The application lifecycle
## The application life cycle
### Loading applications
@@ -210,7 +210,7 @@ defmodule Application do
dependencies for you.
If the application does not have a callback module configured, starting is
done at this point. Otherwise, its `c:start/2` callback if invoked. The PID of
done at this point. Otherwise, its `c:start/2` callback is invoked. The PID of
the top-level supervisor returned by this function is stored by the runtime
for later use, and the returned application state is saved too, if any.
@@ -499,7 +499,7 @@ defmodule Application do
Giving a path is useful to let Elixir know that only certain paths
in a large configuration are compile time dependent.
"""
# TODO: Warn if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
# TODO: Warn on v1.14 if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
@doc since: "1.10.0"
@spec compile_env(app, key | list, value) :: value
defmacro compile_env(app, key_or_path, default \\ nil) when is_atom(app) do
@@ -524,7 +524,7 @@ defmodule Application do
Reads the application environment at compilation time or raises.
This is the same as `compile_env/3` but it raises an
ArgumentError if the configuration is not available.
`ArgumentError` if the configuration is not available.
"""
@doc since: "1.10.0"
@spec compile_env!(app, key | list) :: value
@@ -584,7 +584,7 @@ defmodule Application do
**Important:** if you are writing a library to be used by other developers,
it is generally recommended to avoid the application environment, as the
application environment is effectively a global storage. For more information,
read our [library guidelines](library-guidelines.html).
read our [library guidelines](library-guidelines.md).
## Examples
@@ -618,6 +618,7 @@ defmodule Application do
"""
@spec get_env(app, key, value) :: value
def get_env(app, key, default \\ nil) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
:application.get_env(app, key, default)
end
@@ -628,6 +629,8 @@ defmodule Application do
"""
@spec fetch_env(app, key) :: {:ok, value} | :error
def fetch_env(app, key) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
case :application.get_env(app, key) do
{:ok, value} -> {:ok, value}
:undefined -> :error
@@ -664,9 +667,7 @@ defmodule Application do
"because configuration at #{inspect(key)} was not set"
:undefined ->
"because the application was not loaded/started. If your application " <>
"depends on #{inspect(app)} at runtime, make sure to load/start it or " <>
"list it under :extra_applications in your mix.exs file"
"because the application was not loaded nor configured"
end
end
@@ -689,6 +690,7 @@ defmodule Application do
"""
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
def put_env(app, key, value, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
:application.set_env(app, key, value, opts)
end
@@ -734,9 +736,18 @@ defmodule Application do
"""
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
def delete_env(app, key, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key)
:application.unset_env(app, key, opts)
end
defp maybe_warn_on_app_env_key(_app, key) when is_atom(key),
do: :ok
defp maybe_warn_on_app_env_key(app, key) do
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
end
@doc """
Ensures the given `app` is started.
+2 -2
View File
@@ -111,9 +111,9 @@ defmodule Behaviour do
end
end
defp __behaviour__doc_value(:none), do: nil
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(%{"en" => doc}), do: doc
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(_), do: nil
import unquote(__MODULE__)
end
+19 -7
View File
@@ -61,7 +61,7 @@ defmodule Bitwise do
end
@doc """
Calculates the bitwise NOT of its argument.
Calculates the bitwise NOT of the argument.
Allowed in guard tests. Inlined by the compiler.
@@ -81,7 +81,9 @@ defmodule Bitwise do
end
@doc """
Prefix (unary) operator; calculates the bitwise NOT of its argument.
Bitwise NOT unary operator.
Calculates the bitwise NOT of the argument.
Allowed in guard tests. Inlined by the compiler.
@@ -118,7 +120,9 @@ defmodule Bitwise do
end
@doc """
Infix operator; calculates the bitwise AND of its arguments.
Bitwise AND operator.
Calculates the bitwise AND of its arguments.
Allowed in guard tests. Inlined by the compiler.
@@ -152,7 +156,9 @@ defmodule Bitwise do
end
@doc """
Infix operator; calculates the bitwise OR of its arguments.
Bitwise OR operator.
Calculates the bitwise OR of its arguments.
Allowed in guard tests. Inlined by the compiler.
@@ -186,7 +192,9 @@ defmodule Bitwise do
end
@doc """
Infix operator; calculates the bitwise XOR of its arguments.
Bitwise XOR operator.
Calculates the bitwise XOR of its arguments.
Allowed in guard tests. Inlined by the compiler.
@@ -229,7 +237,9 @@ defmodule Bitwise do
end
@doc """
Infix operator; calculates the result of an arithmetic left bitshift.
Arithmetic left bitshift operator.
Calculates the result of an arithmetic left bitshift.
Allowed in guard tests. Inlined by the compiler.
@@ -281,7 +291,9 @@ defmodule Bitwise do
end
@doc """
Infix operator; calculates the result of an arithmetic right bitshift.
Arithmetic right bitshift operator.
Calculates the result of an arithmetic right bitshift.
Allowed in guard tests. Inlined by the compiler.
+520 -3
View File
@@ -68,10 +68,18 @@ defmodule Calendar do
@typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)"
@type zone_abbr :: String.t()
@typedoc "The time zone UTC offset in seconds"
@typedoc """
The time zone UTC offset in seconds for standard time.
See also `t:std_offset/0`.
"""
@type utc_offset :: integer
@typedoc "The time zone standard offset in seconds (not zero in summer times)"
@typedoc """
The time zone standard offset in seconds (typically not zero in summer times).
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
"""
@type std_offset :: integer
@typedoc "Any map/struct that contains the date fields"
@@ -159,8 +167,14 @@ defmodule Calendar do
@doc """
Calculates the day of the week from the given `year`, `month`, and `day`.
The `starting_on` represents the starting day of the week. All
calendars must support at least the `:default` value. They may
also support other values representing their days of the week.
"""
@callback day_of_week(year, month, day) :: day_of_week()
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
{day_of_week(), first_day_of_week :: non_neg_integer(),
last_day_of_week :: non_neg_integer()}
@doc """
Calculates the day of the year from the given `year`, `month`, and `day`.
@@ -363,4 +377,507 @@ defmodule Calendar do
def get_time_zone_database() do
Application.get_env(:elixir, :time_zone_database, Calendar.UTCOnlyTimeZoneDatabase)
end
@doc """
Formats received datetime into a string.
The datetime can be any of the Calendar types (`Time`, `Date`,
`NaiveDateTime`, and `DateTime`) or any map, as long as they
contain all of the relevant fields necessary for formatting.
For example, if you use `%Y` to format the year, the datetime
must have the `:year` field. Therefore, if you pass a `Time`,
or a map without the `:year` field to a format that expects `%Y`,
an error will be raised.
## Options
* `:preferred_datetime` - a string for the preferred format to show datetimes,
it can't contain the `%c` format and defaults to `"%Y-%m-%d %H:%M:%S"`
if the option is not received
* `:preferred_date` - a string for the preferred format to show dates,
it can't contain the `%x` format and defaults to `"%Y-%m-%d"`
if the option is not received
* `:preferred_time` - a string for the preferred format to show times,
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
if the option is not received
* `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
the name of the period of the day, if the option is not received it defaults
to a function that returns `"am"` and `"pm"`, respectively
* `:month_names` - a function that receives a number and returns the name of
the corresponding month, if the option is not received it defaults to a
function that returns the month names in English
* `:abbreviated_month_names` - a function that receives a number and returns the
abbreviated name of the corresponding month, if the option is not received it
defaults to a function that returns the abbreviated month names in English
* `:day_of_week_names` - a function that receives a number and returns the name of
the corresponding day of week, if the option is not received it defaults to a
function that returns the day of week names in English
* `:abbreviated_day_of_week_names` - a function that receives a number and returns
the abbreviated name of the corresponding day of week, if the option is not received
it defaults to a function that returns the abbreviated day of week names in English
## Formatting syntax
The formatting syntax for strftime is a sequence of characters in the following format:
%<padding><width><format>
where:
* `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section
* `<format>`: the format itself (see below)
### Accepted padding options
* `-`: no padding, removes all padding from the format
* `_`: pad with spaces
* `0`: pad with zeroes
### Accepted formats
The accepted formats are:
Format | Description | Examples (in ISO)
:----- | :-----------------------------------------------------------------------| :------------------------
a | Abbreviated name of day | Mon
A | Full name of day | Monday
b | Abbreviated month name | Jan
B | Full month name | January
c | Preferred date+time representation | 2018-10-17 12:34:56
d | Day of the month | 01, 12
f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
H | Hour using a 24-hour clock | 00, 23
I | Hour using a 12-hour clock | 01, 12
j | Day of the year | 001, 366
m | Month | 01, 12
M | Minute | 00, 59
p | "AM" or "PM" (noon is "PM", midnight as "AM") | AM, PM
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
q | Quarter | 1, 2, 3, 4
S | Second | 00, 59, 60
u | Day of the week | 1 (Monday), 7 (Sunday)
x | Preferred date (without time) representation | 2018-10-17
X | Preferred time (without date) representation | 12:34:56
y | Year as 2-digits | 01, 01, 86, 18
Y | Year | -0001, 0001, 1986
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
Z | Time zone abbreviation (empty string if naive) | CET, BRST
% | Literal "%" character | %
Any other character will be interpreted as an invalid format and raise an error
## Examples
Without options:
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%y-%m-%d %I:%M:%S %p")
"19-08-26 01:52:06 PM"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%a, %B %d %Y")
"Mon, August 26 2019"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c")
"2019-08-26 13:52:06"
With options:
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c", preferred_datetime: "%H:%M:%S %d-%m-%y")
"13:52:06 26-08-19"
iex> Calendar.strftime(
...> ~U[2019-08-26 13:52:06.0Z],
...> "%A",
...> day_of_week_names: fn day_of_week ->
...> {"segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
...> "sexta-feira", "sábado", "domingo"}
...> |> elem(day_of_week - 1)
...> end
...>)
"segunda-feira"
iex> Calendar.strftime(
...> ~U[2019-08-26 13:52:06.0Z],
...> "%B",
...> month_names: fn month ->
...> {"январь", "февраль", "март", "апрель", "май", "июнь",
...> "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"}
...> |> elem(month - 1)
...> end
...>)
"август"
"""
@doc since: "1.11.0"
@spec strftime(map(), String.t(), keyword()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse(
string_format,
date_or_time_or_datetime,
options(user_options),
[]
)
|> IO.iodata_to_binary()
end
defp parse("", _datetime, _format_options, acc),
do: Enum.reverse(acc)
defp parse("%" <> rest, datetime, format_options, acc),
do: parse_modifiers(rest, nil, nil, {datetime, format_options, acc})
defp parse(<<char, rest::binary>>, datetime, format_options, acc),
do: parse(rest, datetime, format_options, [char | acc])
defp parse_modifiers("-" <> rest, width, nil, parser_data) do
parse_modifiers(rest, width, "", parser_data)
end
defp parse_modifiers("0" <> rest, width, nil, parser_data) do
parse_modifiers(rest, width, ?0, parser_data)
end
defp parse_modifiers("_" <> rest, width, nil, parser_data) do
parse_modifiers(rest, width, ?\s, parser_data)
end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
new_width = (width || 0) * 10 + (digit - ?0)
parse_modifiers(rest, new_width, pad, parser_data)
end
# set default padding if none was specified
defp parse_modifiers(<<format, _::binary>> = rest, width, nil, parser_data) do
parse_modifiers(rest, width, default_pad(format), parser_data)
end
# set default width if none was specified
defp parse_modifiers(<<format, _::binary>> = rest, nil, pad, parser_data) do
parse_modifiers(rest, default_width(format), pad, parser_data)
end
defp parse_modifiers(rest, width, pad, {datetime, format_options, acc}) do
format_modifiers(rest, width, pad, datetime, format_options, acc)
end
defp am_pm(hour, format_options) when hour > 11 do
format_options.am_pm_names.(:pm)
end
defp am_pm(hour, format_options) when hour <= 11 do
format_options.am_pm_names.(:am)
end
defp default_pad(format) when format in 'aAbBpPZ', do: ?\s
defp default_pad(_format), do: ?0
defp default_width(format) when format in 'dHImMSy', do: 2
defp default_width(?j), do: 3
defp default_width(format) when format in 'Yz', do: 4
defp default_width(_format), do: 0
# Literally just %
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
end
# Abbreviated name of day
defp format_modifiers("a" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime
|> Date.day_of_week()
|> format_options.abbreviated_day_of_week_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Full name of day
defp format_modifiers("A" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime
|> Date.day_of_week()
|> format_options.day_of_week_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Abbreviated month name
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.month
|> format_options.abbreviated_month_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Full month name
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Preferred date+time representation
defp format_modifiers(
"c" <> _rest,
_width,
_pad,
_datetime,
%{preferred_datetime_invoked: true},
_acc
) do
raise ArgumentError,
"tried to format preferred_datetime within another preferred_datetime format"
end
defp format_modifiers("c" <> rest, width, pad, datetime, format_options, acc) do
result =
format_options.preferred_datetime
|> parse(datetime, %{format_options | preferred_datetime_invoked: true}, [])
|> pad_preferred(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the month
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Microseconds
defp format_modifiers("f" <> rest, _width, _pad, datetime, format_options, acc) do
{microsecond, precision} = datetime.microsecond
result =
microsecond
|> Integer.to_string()
|> String.pad_leading(6, "0")
|> binary_part(0, max(precision, 1))
parse(rest, datetime, format_options, [result | acc])
end
# Hour using a 24-hour clock
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the year
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Month
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Minute
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# "AM" or "PM" (noon is "PM", midnight as "AM")
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# "am" or "pm" (noon is "pm", midnight as "am")
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.hour
|> am_pm(format_options)
|> String.downcase()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Quarter
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Second
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Day of the week
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Preferred date (without time) representation
defp format_modifiers(
"x" <> _rest,
_width,
_pad,
_datetime,
%{preferred_date_invoked: true},
_acc
) do
raise ArgumentError,
"tried to format preferred_date within another preferred_date format"
end
defp format_modifiers("x" <> rest, width, pad, datetime, format_options, acc) do
result =
format_options.preferred_date
|> parse(datetime, %{format_options | preferred_date_invoked: true}, [])
|> pad_preferred(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Preferred time (without date) representation
defp format_modifiers(
"X" <> _rest,
_width,
_pad,
_datetime,
%{preferred_time_invoked: true},
_acc
) do
raise ArgumentError,
"tried to format preferred_time within another preferred_time format"
end
defp format_modifiers("X" <> rest, width, pad, datetime, format_options, acc) do
result =
format_options.preferred_time
|> parse(datetime, %{format_options | preferred_time_invoked: true}, [])
|> pad_preferred(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Year as 2-digits
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
# +hhmm/-hhmm time zone offset from UTC (empty string if naive)
defp format_modifiers(
"z" <> rest,
width,
pad,
datetime = %{utc_offset: utc_offset, std_offset: std_offset},
format_options,
acc
) do
absolute_offset = abs(utc_offset + std_offset)
offset_number =
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
parse(rest, datetime, format_options, [result | acc])
end
defp format_modifiers("z" <> rest, _width, _pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, ["" | acc])
end
# Time zone abbreviation (empty string if naive)
defp format_modifiers("Z" <> rest, width, pad, datetime, format_options, acc) do
result = datetime |> Map.get(:zone_abbr, "") |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
defp format_modifiers(rest, _width, _pad, _datetime, _format_options, _acc) do
{next, _rest} = String.next_grapheme(rest) || {"", ""}
raise ArgumentError, "invalid strftime format: %#{next}"
end
defp pad_preferred(result, width, pad) when length(result) < width do
pad_preferred([pad | result], width, pad)
end
defp pad_preferred(result, _width, _pad), do: result
defp pad_leading(string, count, padding) do
to_pad = count - byte_size(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
defp do_pad_leading(0, _, acc), do: acc
defp do_pad_leading(count, padding, acc),
do: do_pad_leading(count - 1, padding, [padding | acc])
defp options(user_options) do
default_options = %{
preferred_date: "%Y-%m-%d",
preferred_time: "%H:%M:%S",
preferred_datetime: "%Y-%m-%d %H:%M:%S",
am_pm_names: fn
:am -> "am"
:pm -> "pm"
end,
month_names: fn month ->
{"January", "February", "March", "April", "May", "June", "July", "August", "September",
"October", "November", "December"}
|> elem(month - 1)
end,
day_of_week_names: fn day_of_week ->
{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|> elem(day_of_week - 1)
end,
abbreviated_month_names: fn month ->
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|> elem(month - 1)
end,
abbreviated_day_of_week_names: fn day_of_week ->
{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} |> elem(day_of_week - 1)
end,
preferred_datetime_invoked: false,
preferred_date_invoked: false,
preferred_time_invoked: false
}
Enum.reduce(user_options, default_options, fn {key, value}, acc ->
if Map.has_key?(acc, key) do
%{acc | key => value}
else
raise ArgumentError, "unknown option #{inspect(key)} given to Calendar.strftime/3"
end
end)
end
end
+237 -9
View File
@@ -88,20 +88,20 @@ defmodule Date do
"""
@doc since: "1.5.0"
@spec range(Date.t(), Date.t()) :: Date.Range.t()
def range(%Date{calendar: calendar} = first, %Date{calendar: calendar} = last) do
@spec range(Calendar.date(), Calendar.date()) :: Date.Range.t()
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
{first_days, _} = to_iso_days(first)
{last_days, _} = to_iso_days(last)
%Date.Range{
first: first,
last: last,
first: %Date{calendar: calendar, year: first.year, month: first.month, day: first.day},
last: %Date{calendar: calendar, year: last.year, month: last.month, day: last.day},
first_in_iso_days: first_days,
last_in_iso_days: last_days
}
end
def range(%Date{}, %Date{}) do
def range(%{calendar: _, year: _, month: _, day: _}, %{calendar: _, year: _, month: _, day: _}) do
raise ArgumentError, "both dates must have matching calendars"
end
@@ -224,6 +224,33 @@ defmodule Date do
end
end
@doc """
Builds a new ISO date.
Expects all values to be integers. Returns `date` if each
entry fits its appropriate range, raises if the date is invalid.
## Examples
iex> Date.new!(2000, 1, 1)
~D[2000-01-01]
iex> Date.new!(2000, 13, 1)
** (ArgumentError) cannot build date, reason: :invalid_date
iex> Date.new!(2000, 2, 29)
~D[2000-02-29]
"""
@doc since: "1.11.0"
@spec new!(Calendar.year(), Calendar.month(), Calendar.day(), Calendar.calendar()) :: t
def new!(year, month, day, calendar \\ Calendar.ISO) do
case new(year, month, day, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot build date, reason: #{inspect(reason)}"
end
end
@doc """
Converts the given date to a string according to its calendar.
@@ -398,6 +425,45 @@ defmodule Date do
end
end
@doc """
Converts a number of gregorian days to a `Date` struct.
## Examples
iex> Date.from_gregorian_days(1)
~D[0000-01-02]
iex> Date.from_gregorian_days(730_485)
~D[2000-01-01]
iex> Date.from_gregorian_days(-1)
~D[-0001-12-31]
"""
@doc since: "1.11.0"
@spec from_gregorian_days(integer(), Calendar.calendar()) :: t
def from_gregorian_days(days, calendar \\ Calendar.ISO) when is_integer(days) do
from_iso_days({days, 0}, calendar)
end
@doc """
Converts a `date` struct to a number of gregorian days.
## Examples
iex> Date.to_gregorian_days(~D[0000-01-02])
1
iex> Date.to_gregorian_days(~D[2000-01-01])
730_485
iex> Date.to_gregorian_days(~N[2000-01-01 00:00:00])
730_485
"""
@doc since: "1.11.0"
@spec to_gregorian_days(Calendar.date()) :: integer()
def to_gregorian_days(date) do
{days, _} = to_iso_days(date)
days
end
@doc """
Compares two date structs.
@@ -618,6 +684,11 @@ defmodule Date do
calendar (the default), it is an integer from 1 to 7, where
1 is Monday and 7 is Sunday.
An optional `starting_on` value may be supplied, which
configures the weekday the week starts on. The default value
for it is `:default`, which translates to `:monday` for the
built-in ISO calendar. Any other weekday may be given to.
## Examples
iex> Date.day_of_week(~D[2016-10-31])
@@ -629,13 +700,131 @@ defmodule Date do
iex> Date.day_of_week(~D[-0015-10-30])
3
iex> Date.day_of_week(~D[2016-10-31], :sunday)
2
iex> Date.day_of_week(~D[2016-11-01], :sunday)
3
iex> Date.day_of_week(~N[2016-11-01 01:23:45], :sunday)
3
iex> Date.day_of_week(~D[-0015-10-30], :sunday)
4
"""
@doc since: "1.4.0"
@spec day_of_week(Calendar.date()) :: Calendar.day()
def day_of_week(date)
@spec day_of_week(Calendar.date(), starting_on :: :default | atom) :: Calendar.day_of_week()
def day_of_week(date, starting_on \\ :default)
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.day_of_week(year, month, day)
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}, starting_on) do
{day_of_week, _first, _last} = calendar.day_of_week(year, month, day, starting_on)
day_of_week
end
@doc """
Calculates a date that is the first day of the week for the given `date`.
If the day is already the first day of the week, it returns the
day itself. For the built-in ISO calendar, the week starts on Monday.
A weekday rather than `:default` can be given as `starting_on`.
## Examples
iex> Date.beginning_of_week(~D[2020-07-11])
~D[2020-07-06]
iex> Date.beginning_of_week(~D[2020-07-06])
~D[2020-07-06]
iex> Date.beginning_of_week(~D[2020-07-11], :sunday)
~D[2020-07-05]
iex> Date.beginning_of_week(~D[2020-07-11], :saturday)
~D[2020-07-11]
iex> Date.beginning_of_week(~N[2020-07-11 01:23:45])
~D[2020-07-06]
"""
@doc since: "1.11.0"
@spec beginning_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
def beginning_of_week(date, starting_on \\ :default)
def beginning_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
%{year: year, month: month, day: day} = date
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
{year, month, day} =
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
1 ->
{year, month, day}
day_of_week ->
Calendar.ISO.date_from_iso_days(iso_days - day_of_week + 1)
end
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
def beginning_of_week(%{calendar: calendar} = date, starting_on) do
%{year: year, month: month, day: day} = date
case calendar.day_of_week(year, month, day, starting_on) do
{day_of_week, day_of_week, _} ->
%Date{calendar: calendar, year: year, month: month, day: day}
{day_of_week, first_day_of_week, _} ->
add(date, -(day_of_week - first_day_of_week))
end
end
@doc """
Calculates a date that is the last day of the week for the given `date`.
If the day is already the last day of the week, it returns the
day itself. For the built-in ISO calendar, the week ends on Sunday.
A weekday rather than `:default` can be given as `starting_on`.
## Examples
iex> Date.end_of_week(~D[2020-07-11])
~D[2020-07-12]
iex> Date.end_of_week(~D[2020-07-05])
~D[2020-07-05]
iex> Date.end_of_week(~D[2020-07-06], :sunday)
~D[2020-07-11]
iex> Date.end_of_week(~D[2020-07-06], :sunday)
~D[2020-07-11]
iex> Date.end_of_week(~D[2020-07-06], :saturday)
~D[2020-07-10]
iex> Date.end_of_week(~N[2020-07-11 01:23:45])
~D[2020-07-12]
"""
@doc since: "1.11.0"
@spec end_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
def end_of_week(date, starting_on \\ :default)
def end_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
%{year: year, month: month, day: day} = date
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
{year, month, day} =
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
7 ->
{year, month, day}
day_of_week ->
Calendar.ISO.date_from_iso_days(iso_days + 7 - day_of_week)
end
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
def end_of_week(%{calendar: calendar} = date, starting_on) do
%{year: year, month: month, day: day} = date
case calendar.day_of_week(year, month, day, starting_on) do
{day_of_week, _, day_of_week} ->
%Date{calendar: calendar, year: year, month: month, day: day}
{day_of_week, _, last_day_of_week} ->
add(date, last_day_of_week - day_of_week)
end
end
@doc """
@@ -739,6 +928,45 @@ defmodule Date do
calendar.day_of_era(year, month, day)
end
@doc """
Calculates a date that is the first day of the month for the given `date`.
## Examples
iex> Date.beginning_of_month(~D[2000-01-31])
~D[2000-01-01]
iex> Date.beginning_of_month(~D[2000-01-01])
~D[2000-01-01]
iex> Date.beginning_of_month(~N[2000-01-31 01:23:45])
~D[2000-01-01]
"""
@doc since: "1.11.0"
@spec beginning_of_month(Calendar.date()) :: t()
def beginning_of_month(%{year: year, month: month, calendar: calendar}) do
%Date{year: year, month: month, day: 1, calendar: calendar}
end
@doc """
Calculates a date that is the last day of the month for the given `date`.
## Examples
iex> Date.end_of_month(~D[2000-01-01])
~D[2000-01-31]
iex> Date.end_of_month(~D[2000-01-31])
~D[2000-01-31]
iex> Date.end_of_month(~N[2000-01-01 01:23:45])
~D[2000-01-31]
"""
@doc since: "1.11.0"
@spec end_of_month(Calendar.date()) :: t()
def end_of_month(%{year: year, month: month, calendar: calendar} = date) do
day = Date.days_in_month(date)
%Date{year: year, month: month, day: day, calendar: calendar}
end
## Helpers
defimpl String.Chars do
+275 -7
View File
@@ -25,10 +25,14 @@ defmodule DateTime do
datetimes and returns `{:error, :utc_only_time_zone_database}`
for any other time zone.
Other time zone databases can also be configured. For example, to use the
[tzdata](https://hexdocs.pm/tzdata/) database, first make sure it is added as
a dependency in `mix.exs`. It can then be configured either via
configuration:
Other time zone databases can also be configured. For example,
two of the available options are:
* [`tz`](https://hexdocs.pm/tz/)
* [`tzdata`](https://hexdocs.pm/tzdata/)
To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration:
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
@@ -36,6 +40,7 @@ defmodule DateTime do
Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)
See the proper names in the library installation instructions.
"""
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
@@ -72,6 +77,7 @@ defmodule DateTime do
}
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1})
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current datetime in UTC.
@@ -88,12 +94,165 @@ defmodule DateTime do
System.os_time() |> from_unix!(:native, calendar)
end
@doc """
Builds a datetime from date and time structs.
It expects a time zone to put the `DateTime` in.
If the time zone is not passed it will default to `"Etc/UTC"`,
which always succeeds. Otherwise, the `DateTime` is checked against the time zone database
given as `time_zone_database`. See the "Time zone database"
section in the module documentation.
## Examples
iex> DateTime.new(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
{:ok, ~U[2016-05-24 13:26:08.003Z]}
When the datetime is ambiguous - for instance during changing from summer
to winter time - the two possible valid datetimes are returned. First the one
that happens first, then the one that happens after.
iex> {:ambiguous, first_dt, second_dt} = DateTime.new(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> first_dt
#DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen>
iex> second_dt
#DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
When there is a gap in wall time - for instance in spring when the clocks are
turned forward - the latest valid datetime just before the gap and the first
valid datetime just after the gap.
iex> {:gap, just_before, just_after} = DateTime.new(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> just_before
#DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen>
iex> just_after
#DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
Most of the time there is one, and just one, valid datetime for a certain
date and time in a certain time zone.
iex> {:ok, datetime} = DateTime.new(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> datetime
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
"""
@doc since: "1.11.0"
@spec new(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) ::
{:ok, t}
| {:ambiguous, t, t}
| {:gap, t, t}
| {:error,
:incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database}
def new(
date,
time,
time_zone \\ "Etc/UTC",
time_zone_database \\ Calendar.get_time_zone_database()
)
def new(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time, "Etc/UTC", _db) do
%{year: year, month: month, day: day} = date
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
datetime = %DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
{:ok, datetime}
end
def new(date, time, time_zone, time_zone_database) do
with {:ok, naive_datetime} <- NaiveDateTime.new(date, time) do
from_naive(naive_datetime, time_zone, time_zone_database)
end
end
@doc """
Builds a datetime from date and time structs, raising on errors.
It expects a time zone to put the `DateTime` in.
If the time zone is not passed it will default to `"Etc/UTC"`,
which always succeeds. Otherwise, the DateTime is checked against the time zone database
given as `time_zone_database`. See the "Time zone database"
section in the module documentation.
## Examples
iex> DateTime.new!(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
~U[2016-05-24 13:26:08.003Z]
When the datetime is ambiguous - for instance during changing from summer
to winter time - an error will be raised.
iex> DateTime.new!(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
** (ArgumentError) cannot build datetime with ~D[2018-10-28] and ~T[02:30:00] because such instant is ambiguous in time zone Europe/Copenhagen as there is an overlap between #DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen> and #DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
When there is a gap in wall time - for instance in spring when the clocks are
turned forward - an error will be raised.
iex> DateTime.new!(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
** (ArgumentError) cannot build datetime with ~D[2019-03-31] and ~T[02:30:00] because such instant does not exist in time zone Europe/Copenhagen as there is a gap between #DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen> and #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
Most of the time there is one, and just one, valid datetime for a certain
date and time in a certain time zone.
iex> datetime = DateTime.new!(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> datetime
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
"""
@doc since: "1.11.0"
@spec new!(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) :: t
def new!(
date,
time,
time_zone \\ "Etc/UTC",
time_zone_database \\ Calendar.get_time_zone_database()
)
def new!(date, time, time_zone, time_zone_database) do
case new(date, time, time_zone, time_zone_database) do
{:ok, datetime} ->
datetime
{:ambiguous, dt1, dt2} ->
raise ArgumentError,
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
"instant is ambiguous in time zone #{time_zone} as there is an overlap " <>
"between #{inspect(dt1)} and #{inspect(dt2)}"
{:gap, dt1, dt2} ->
raise ArgumentError,
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
"instant does not exist in time zone #{time_zone} as there is a gap " <>
"between #{inspect(dt1)} and #{inspect(dt2)}"
{:error, reason} ->
raise ArgumentError,
"cannot build datetime with #{inspect(date)} and #{inspect(time)}, reason: #{
inspect(reason)
}"
end
end
@doc """
Converts the given Unix time to `DateTime`.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
be converted to microseconds internally.
be converted to microseconds internally. Up to
253402300799 seconds is supported.
Unix times are always in UTC and therefore the DateTime
will be returned in UTC.
@@ -108,14 +267,26 @@ defmodule DateTime do
iex> datetime
~U[2015-05-25 13:26:08.868569Z]
iex> {:ok, datetime} = DateTime.from_unix(253_402_300_799)
iex> datetime
~U[9999-12-31 23:59:59Z]
iex> {:error, :invalid_unix_time} = DateTime.from_unix(253_402_300_800)
The unit can also be an integer as in `t:System.time_unit/0`:
iex> {:ok, datetime} = DateTime.from_unix(143_256_036_886_856, 1024)
iex> datetime
~U[6403-03-17 07:05:22.320312Z]
Negative Unix times are supported, up to -62167219200 seconds,
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
Negative Unix times are supported up to -377705116800 seconds:
iex> {:ok, datetime} = DateTime.from_unix(-377_705_116_800)
iex> datetime
~U[-9999-01-01 00:00:00Z]
iex> {:error, :invalid_unix_time} = DateTime.from_unix(-377_705_116_801)
"""
@spec from_unix(integer, :native | System.time_unit(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
@@ -582,6 +753,10 @@ defmodule DateTime do
It will return the integer with the given unit,
according to `System.convert_time_unit/3`.
If you want to get the current time in Unix seconds,
do not do `DateTime.utc_now() |> DateTime.to_unix()`.
Simply call `System.os_time(:second)` instead.
## Examples
iex> 1_464_096_368 |> DateTime.from_unix!() |> DateTime.to_unix()
@@ -863,6 +1038,93 @@ defmodule DateTime do
end
end
@doc """
Converts a number of gregorian seconds to a `DateTime` struct.
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
`DateTime.shift_zone/3`.
## Examples
iex> DateTime.from_gregorian_seconds(1)
~U[0000-01-01 00:00:01Z]
iex> DateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
~U[2020-05-01 00:26:31.005Z]
iex> DateTime.from_gregorian_seconds(-1)
~U[-0001-12-31 23:59:59Z]
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days)
%DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
end
@doc """
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
## Examples
iex> dt = %DateTime{year: 0000, month: 1, day: 1, zone_abbr: "UTC",
...> hour: 0, minute: 0, second: 1, microsecond: {0, 0},
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
iex> DateTime.to_gregorian_seconds(dt)
{1, 0}
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "UTC",
...> hour: 0, minute: 26, second: 31, microsecond: {5000, 0},
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
iex> DateTime.to_gregorian_seconds(dt)
{63_755_511_991, 5000}
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "CET",
...> hour: 1, minute: 26, second: 31, microsecond: {5000, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_gregorian_seconds(dt)
{63_755_511_991, 5000}
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(
%{
std_offset: std_offset,
utc_offset: utc_offset,
microsecond: {microsecond, _}
} = datetime
) do
{days, day_fraction} =
datetime
|> to_iso_days()
|> apply_tz_offset(utc_offset + std_offset)
seconds_in_day = seconds_from_day_fraction(day_fraction)
{days * @seconds_per_day + seconds_in_day, microsecond}
end
@doc """
Converts the given `datetime` to a string according to its calendar.
@@ -1263,6 +1525,12 @@ defmodule DateTime do
}
end
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
do: parts_in_day
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
do: div(parts_in_day * @seconds_per_day, parts_per_day)
defimpl String.Chars do
def to_string(datetime) do
%{
+86 -31
View File
@@ -8,6 +8,12 @@ defmodule Calendar.ISO do
applied for all time, consequently the dates give different results
before the year 1583 from when the Gregorian calendar was adopted.
Given this is the default calendar used by Elixir, it has one
difference compared to the ISO8601 specification in that it allows
a whitespace instead of `T` as a seperator between date and times
both when parsing and formatting. Strict formatting can be done
by using the `to_iso8601` found in `NaiveDateTime` and `DateTime`.
Note that while ISO 8601 allows times and datetimes to specify
24:00:00 as the zero hour of the next day, this notation is not
supported by Elixir.
@@ -44,6 +50,7 @@ defmodule Calendar.ISO do
@type hour :: 0..23
@type minute :: 0..59
@type second :: 0..59
@type weekday :: :monday | :tuesday | :wednesday | :thursday | :friday | :saturday | :sunday
@typedoc """
Microseconds with stored precision.
@@ -493,12 +500,12 @@ defmodule Calendar.ISO do
total_microseconds = divide_by_parts_per_day(parts_in_day, parts_per_day)
{hours, rest_microseconds1} =
div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)
div_rem(total_microseconds, @seconds_per_hour * @microseconds_per_second)
{minutes, rest_microseconds2} =
div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
div_rem(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
{seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
{seconds, microseconds} = div_rem(rest_microseconds2, @microseconds_per_second)
{hours, minutes, seconds, {microseconds, 6}}
end
@@ -533,7 +540,7 @@ defmodule Calendar.ISO do
{year, month, day_in_month + 1}
end
defp div_mod(int1, int2) do
defp div_rem(int1, int2) do
div = div(int1, int2)
rem = int1 - div * int2
@@ -619,43 +626,83 @@ defmodule Calendar.ISO do
rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0)
end
# TODO: Deprecate me on v1.15
@doc false
def day_of_week(year, month, day) do
day_of_week(year, month, day, :default) |> elem(0)
end
@doc """
Calculates the day of the week from the given `year`, `month`, and `day`.
It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.
It is an integer from 1 to 7, where 1 is the given `starting_on` weekday.
For example, if `starting_on` is set to `:monday`, then 1 is Monday and
7 is Sunday.
`starting_on` can also be `:default`, which is equivalent to `:monday`.
## Examples
iex> Calendar.ISO.day_of_week(2016, 10, 31)
1
iex> Calendar.ISO.day_of_week(2016, 11, 1)
2
iex> Calendar.ISO.day_of_week(2016, 11, 2)
3
iex> Calendar.ISO.day_of_week(2016, 11, 3)
4
iex> Calendar.ISO.day_of_week(2016, 11, 4)
5
iex> Calendar.ISO.day_of_week(2016, 11, 5)
6
iex> Calendar.ISO.day_of_week(2016, 11, 6)
7
iex> Calendar.ISO.day_of_week(-99, 1, 31)
4
iex> Calendar.ISO.day_of_week(2016, 10, 31, :monday)
{1, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 1, :monday)
{2, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 2, :monday)
{3, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 3, :monday)
{4, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 4, :monday)
{5, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 5, :monday)
{6, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 6, :monday)
{7, 1, 7}
iex> Calendar.ISO.day_of_week(-99, 1, 31, :monday)
{4, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 10, 31, :sunday)
{2, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 1, :sunday)
{3, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 2, :sunday)
{4, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 3, :sunday)
{5, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 4, :sunday)
{6, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 5, :sunday)
{7, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 11, 6, :sunday)
{1, 1, 7}
iex> Calendar.ISO.day_of_week(-99, 1, 31, :sunday)
{5, 1, 7}
iex> Calendar.ISO.day_of_week(2016, 10, 31, :saturday)
{3, 1, 7}
"""
@doc since: "1.4.0"
@spec day_of_week(year, month, day) :: day_of_week()
@doc since: "1.11.0"
@spec day_of_week(year, month, day, :default | weekday) :: {day_of_week(), 1, 7}
@impl true
def day_of_week(year, month, day) do
date_to_iso_days(year, month, day)
|> iso_days_to_day_of_week()
def day_of_week(year, month, day, starting_on) do
iso_days = date_to_iso_days(year, month, day)
{iso_days_to_day_of_week(iso_days, starting_on), 1, 7}
end
defp iso_days_to_day_of_week(iso_days) do
Integer.mod(iso_days + 5, 7) + 1
@doc false
def iso_days_to_day_of_week(iso_days, starting_on) do
Integer.mod(iso_days + day_of_week_offset(starting_on), 7) + 1
end
defp day_of_week_offset(:default), do: 5
defp day_of_week_offset(:wednesday), do: 3
defp day_of_week_offset(:thursday), do: 2
defp day_of_week_offset(:friday), do: 1
defp day_of_week_offset(:saturday), do: 0
defp day_of_week_offset(:sunday), do: 6
defp day_of_week_offset(:monday), do: 5
defp day_of_week_offset(:tuesday), do: 4
@doc """
Calculates the day of the year from the given `year`, `month`, and `day`.
@@ -1160,6 +1207,14 @@ defmodule Calendar.ISO do
end
end
@doc false
def gregorian_seconds_to_iso_days(seconds, microsecond) do
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
microseconds_in_day = rest_seconds * @microseconds_per_second + microsecond
day_fraction = {microseconds_in_day, @parts_per_day}
{days, day_fraction}
end
@doc false
def iso_days_to_unit({days, {parts, ppd}}, unit) do
day_microseconds = days * @parts_per_day
@@ -1312,7 +1367,7 @@ defmodule Calendar.ISO do
end
defp iso_seconds_to_datetime(seconds) do
{days, rest_seconds} = div_mod(seconds, @seconds_per_day)
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
date = date_from_iso_days(days)
time = seconds_to_time(rest_seconds)
@@ -1320,8 +1375,8 @@ defmodule Calendar.ISO do
end
defp seconds_to_time(seconds) when seconds in 0..@last_second_of_the_day do
{hour, rest_seconds} = div_mod(seconds, @seconds_per_hour)
{minute, second} = div_mod(rest_seconds, @seconds_per_minute)
{hour, rest_seconds} = div_rem(seconds, @seconds_per_hour)
{minute, second} = div_rem(rest_seconds, @seconds_per_minute)
{hour, minute, second}
end
+157
View File
@@ -78,6 +78,8 @@ defmodule NaiveDateTime do
microsecond: Calendar.microsecond()
}
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current naive datetime in UTC.
@@ -242,6 +244,61 @@ defmodule NaiveDateTime do
end
end
@doc """
Builds a new ISO naive datetime.
Expects all values to be integers. Returns `naive_datetime`
if each entry fits its appropriate range, raises if
time or date is invalid.
## Examples
iex> NaiveDateTime.new!(2000, 1, 1, 0, 0, 0)
~N[2000-01-01 00:00:00]
iex> NaiveDateTime.new!(2000, 2, 29, 0, 0, 0)
~N[2000-02-29 00:00:00]
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1})
~N[2000-01-01 23:59:59.0]
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, 999_999)
~N[2000-01-01 23:59:59.999999]
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1}, Calendar.ISO)
~N[2000-01-01 23:59:59.0]
iex> NaiveDateTime.new!(2000, 1, 1, 24, 59, 59, 999_999)
** (ArgumentError) cannot build naive datetime, reason: :invalid_time
"""
@doc since: "1.11.0"
@spec new!(
Calendar.year(),
Calendar.month(),
Calendar.day(),
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(
year,
month,
day,
hour,
minute,
second,
microsecond \\ {0, 0},
calendar \\ Calendar.ISO
)
def new!(year, month, day, hour, minute, second, microsecond, calendar) do
case new(year, month, day, hour, minute, second, microsecond, calendar) do
{:ok, naive_datetime} ->
naive_datetime
{:error, reason} ->
raise ArgumentError, "cannot build naive datetime, reason: #{inspect(reason)}"
end
end
@doc """
Builds a naive datetime from date and time structs.
@@ -272,6 +329,24 @@ defmodule NaiveDateTime do
{:ok, naive_datetime}
end
@doc """
Builds a naive datetime from date and time structs.
## Examples
iex> NaiveDateTime.new!(~D[2010-01-13], ~T[23:00:07.005])
~N[2010-01-13 23:00:07.005]
"""
@doc since: "1.11.0"
@spec new!(Date.t(), Time.t()) :: t
def new!(date, time)
def new!(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time) do
{:ok, naive_datetime} = new(date, time)
naive_datetime
end
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
@@ -769,6 +844,82 @@ defmodule NaiveDateTime do
end
end
@doc """
Converts a number of gregorian seconds to a `NaiveDateTime` struct.
## Examples
iex> NaiveDateTime.from_gregorian_seconds(1)
~N[0000-01-01 00:00:01]
iex> NaiveDateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
~N[2020-05-01 00:26:31.005]
iex> NaiveDateTime.from_gregorian_seconds(-1)
~N[-0001-12-31 23:59:59]
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days)
%NaiveDateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}
end
@doc """
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
## Examples
iex> NaiveDateTime.to_gregorian_seconds(~N[0000-01-01 00:00:01])
{1, 0}
iex> NaiveDateTime.to_gregorian_seconds(~N[2020-05-01 00:26:31.005])
{63_755_511_991, 5000}
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
month,
day,
hour,
minute,
second,
{microsecond, precision}
)
seconds_in_day = seconds_from_day_fraction(day_fraction)
{days * @seconds_per_day + seconds_in_day, microsecond}
end
@doc """
Compares two `NaiveDateTime` structs.
@@ -913,6 +1064,12 @@ defmodule NaiveDateTime do
## Helpers
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
do: parts_in_day
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
do: div(parts_in_day * @seconds_per_day, parts_per_day)
# Keep it multiline for proper function clause errors.
defp to_iso_days(%{
calendar: calendar,
+98 -2
View File
@@ -46,6 +46,7 @@ defmodule Time do
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current time in UTC.
@@ -139,6 +140,44 @@ defmodule Time do
end
end
@doc """
Builds a new time.
Expects all values to be integers. Returns `time` if each
entry fits its appropriate range, raises if the time is invalid.
Microseconds can also be given with a precision, which must be an
integer between 0 and 6.
The built-in calendar does not support leap seconds.
## Examples
iex> Time.new!(0, 0, 0, 0)
~T[00:00:00.000000]
iex> Time.new!(23, 59, 59, 999_999)
~T[23:59:59.999999]
iex> Time.new!(24, 59, 59, 999_999)
** (ArgumentError) cannot build time, reason: :invalid_time
"""
@doc since: "1.11.0"
@spec new!(
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case new(hour, minute, second, microsecond, calendar) do
{:ok, time} ->
time
{:error, reason} ->
raise ArgumentError, "cannot build time, reason: #{inspect(reason)}"
end
end
@doc """
Converts the given `time` to a string.
@@ -358,6 +397,63 @@ defmodule Time do
end
end
@doc """
Converts a number of seconds after midnight to a `Time` struct.
## Examples
iex> Time.from_seconds_after_midnight(10_000)
~T[02:46:40]
iex> Time.from_seconds_after_midnight(30_000, {5000, 3})
~T[08:20:00.005]
iex> Time.from_seconds_after_midnight(-1)
~T[23:59:59]
iex> Time.from_seconds_after_midnight(100_000)
~T[03:46:40]
"""
@doc since: "1.11.0"
@spec from_seconds_after_midnight(
integer(),
Calendar.microsecond(),
Calendar.calendar()
) :: t
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
when is_integer(seconds) do
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
{hour, minute, second, {_, _}} =
calendar.time_from_day_fraction({seconds_in_day, @seconds_per_day})
%Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
end
@doc """
Converts a `Time` struct to a number of seconds after midnight.
The returned value is a two-element tuple with the number of seconds and microseconds.
## Examples
iex> Time.to_seconds_after_midnight(~T[23:30:15])
{84615, 0}
iex> Time.to_seconds_after_midnight(~N[2010-04-17 23:30:15.999])
{84615, 999000}
"""
@doc since: "1.11.0"
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
iso_days = {0, to_day_fraction(time)}
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
end
@doc """
Adds the `number` of `unit`s to the given `time`.
@@ -556,12 +652,12 @@ defmodule Time do
As with the `compare/2` function both `Time` structs and other structures
containing time can be used. If for instance a `NaiveDateTime` or `DateTime`
is passed, only the hour, month, second, and microsecond is considered. Any
is passed, only the hour, minute, second, and microsecond is considered. Any
additional information about a date or time zone is ignored when calculating
the difference.
The answer can be returned in any `unit` available from
`t:System.time_unit/0`. If the first unit is smaller than
`t:System.time_unit/0`. If the first time value is earlier than
the second, a negative number is returned.
This function returns the difference in seconds where seconds
@@ -59,7 +59,7 @@ defmodule Calendar.TimeZoneDatabase do
with a limit for when the period ends (wall time). The second nested two-tuple is the period
just after the gap and a datetime (wall time) for when the period begins just after the gap.
If there is only a single possible period for the provided `datetime`, the a tuple with `:single`
If there is only a single possible period for the provided `datetime`, then a tuple with `:ok`
and the `time_zone_period` is returned.
"""
@doc since: "1.8.0"
+94 -30
View File
@@ -43,6 +43,15 @@ defmodule Code do
You can configure your list of tracers via `put_compiler_option/2`. The
following events are available to tracers:
* `:start` - (since v1.11.0) invoked whenever the compiler starts to trace
a new lexical context, such as a new file. Keep in mind the compiler runs
in parallel, so multiple files may invoke `:start` and run at the same
time. The value of the `lexical_tracker` of the macro environment, albeit
opaque, can be used to uniquely identify the environment.
* `:stop` - (since v1.11.0) invoked whenever the compiler stops tracing a
new lexical context, such as a new file.
* `{:import, meta, module, opts}` - traced whenever `module` is imported.
`meta` is the import AST metadata and `opts` are the import options.
@@ -75,14 +84,14 @@ defmodule Code do
function or macro is referenced. `meta` is the call AST metadata, `module`
is the invoked module, followed by the `name` and `arity`.
* `{:local_function, meta, module, name, arity}` and
`{:local_macro, meta, module, name, arity}` - traced whenever a local
function or macro is referenced. `meta` is the call AST metadata, `module`
is the invoked module, followed by the `name` and `arity`.
* `{:local_function, meta, name, arity}` and
`{:local_macro, meta, name, arity}` - traced whenever a local
function or macro is referenced. `meta` is the call AST metadata, followed by
the `name` and `arity`.
* `{:compile_env, app, path, return}` - traced whenever `Application.compile_env/3`
or `Application.compile_env!/2` are called. `app` is an atom, `path` is a list
of keys to traverse in the application environemnt and `return` is either
of keys to traverse in the application environment and `return` is either
`{:ok, value}` or `:error`.
The `:tracers` compiler option can be combined with the `:parser_options`
@@ -162,14 +171,14 @@ defmodule Code do
# Now unrequire all files
Code.unrequire_files(Code.required_files())
# Notice modules are still available
# Note that modules are still available
function_exported?(EExTest.Compiled, :before_compile, 0)
#=> true
"""
@doc since: "1.7.0"
@spec unrequire_files([binary]) :: :ok
def unrequire_files(files) do
def unrequire_files(files) when is_list(files) do
:elixir_code_server.cast({:unrequire_files, files})
end
@@ -281,7 +290,7 @@ defmodule Code do
and the second a list of imported macro names and arity; the list
of function names and arity must be sorted
Notice that setting any of the values above overrides Elixir's default
Note that setting any of the values above overrides Elixir's default
values. For example, setting `:requires` to `[]` will no longer
automatically require the `Kernel` module. In the same way setting
`:macros` will no longer auto-import `Kernel` macros like `Kernel.if/2`,
@@ -328,7 +337,7 @@ defmodule Code do
defp eval_string_with_error_handling(string, binding, opts) do
%{line: line, file: file} = env = :elixir.env_for_eval(opts)
forms = :elixir.string_to_quoted!(to_charlist(string), line, file, [])
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = :elixir.eval_forms(forms, binding, env)
{value, binding}
end
@@ -366,7 +375,7 @@ defmodule Code do
* `:force_do_end_blocks` (since v1.9.0) - when `true`, converts all
inline usages of `do: ...`, `else: ...` and friends into `do/end`
blocks. Defaults to `false`. Notice this option is convergent:
blocks. Defaults to `false`. Note that this option is convergent:
once you set it to `true`, all keywords will be converted. If you
set it to `false` later on, `do/end` blocks won't be converted
back to keywords.
@@ -750,6 +759,9 @@ defmodule Code do
* `:line` - the starting line of the string being parsed.
Defaults to 1.
* `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to 1.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -817,12 +829,13 @@ defmodule Code do
"""
@spec string_to_quoted(List.Chars.t(), keyword) ::
{:ok, Macro.t()} | {:error, {line :: pos_integer, term, term}}
{:ok, Macro.t()} | {:error, {location :: keyword, term, term}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir.string_to_tokens(to_charlist(string), line, file, opts) do
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
@@ -845,7 +858,8 @@ defmodule Code do
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
:elixir.string_to_quoted!(to_charlist(string), line, file, opts)
column = Keyword.get(opts, :column, 1)
:elixir.string_to_quoted!(to_charlist(string), line, column, file, opts)
end
@doc """
@@ -880,7 +894,7 @@ defmodule Code do
If the file was already required, `require_file/2` doesn't do anything and
returns `nil`.
Notice that if `require_file/2` is invoked by different processes concurrently,
Note that if `require_file/2` is invoked by different processes concurrently,
the first process to invoke `require_file/2` acquires a lock and the remaining
ones will block until the file is available. This means that if `require_file/2`
is called more than once with a given file, that file will be compiled only once.
@@ -923,7 +937,7 @@ defmodule Code do
@doc """
Gets all compilation options from the code server.
To get invidual options, see `get_compiler_option/1`.
To get individual options, see `get_compiler_option/1`.
For a description of all options, see `put_compiler_option/2`.
## Examples
@@ -942,7 +956,7 @@ defmodule Code do
@doc """
Stores all given compilation options.
To store invidual options, see `put_compiler_option/2`.
To store individual options, see `put_compiler_option/2`.
For a description of all options, see `put_compiler_option/2`.
## Examples
@@ -954,8 +968,9 @@ defmodule Code do
@spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
def compiler_options(opts) do
for {key, value} <- opts, into: %{} do
previous = get_compiler_option(key)
put_compiler_option(key, value)
{key, value}
{key, previous}
end
end
@@ -1266,7 +1281,7 @@ defmodule Code do
def ensure_compiled(module) when is_atom(module) do
case :code.ensure_loaded(module) do
{:error, :nofile} = error ->
if is_pid(:erlang.get(:elixir_compiler_pid)) do
if can_await_module_compilation?() do
case Kernel.ErrorHandler.ensure_compiled(module, :module, :soft) do
:found -> {:module, module}
:deadlock -> {:error, :unavailable}
@@ -1281,6 +1296,21 @@ defmodule Code do
end
end
@doc """
Returns true if the current process can await for module compilation.
When compiling Elixir code via `Kernel.ParallelCompiler`, which is
used by Mix and `elixirc`, calling a module that has not yet been
compiled will block the caller until the module becomes available.
Executing Elixir scripts, such as passing a filename to `elixir`,
does not await.
"""
@doc since: "1.11.0"
@spec can_await_module_compilation? :: boolean
def can_await_module_compilation? do
:erlang.process_info(self(), :error_handler) == {:error_handler, Kernel.ErrorHandler}
end
@doc false
@deprecated "Use Code.ensure_compiled/1 instead (see the proper disclaimers in its docs)"
def ensure_compiled?(module) when is_atom(module) do
@@ -1317,8 +1347,8 @@ defmodule Code do
docs :: [doc_element]}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | :lfe | :alpaca | atom(),
doc_content: %{required(binary) => binary} | :none | :hidden,
beam_language: :elixir | :erlang | atom(),
doc_content: %{optional(binary) => binary} | :none | :hidden,
doc_element:
{{kind :: atom, function_name :: atom, arity}, annotation, signature, doc_content,
metadata},
@@ -1329,31 +1359,65 @@ defmodule Code do
def fetch_docs(module) when is_atom(module) do
case :code.get_object_code(module) do
{_module, bin, _beam_path} -> do_fetch_docs(bin)
:error -> {:error, :module_not_found}
{_module, bin, beam_path} ->
case fetch_docs_from_beam(bin) do
{:error, :chunk_not_found} ->
app_root = Path.expand(Path.join(["..", ".."]), beam_path)
path = Path.join([app_root, "doc", "chunks", "#{module}.chunk"])
fetch_docs_from_chunk(path)
other ->
other
end
:error ->
case :code.which(module) do
:preloaded ->
path = Path.join([:code.lib_dir(:erts), "doc", "chunks", "#{module}.chunk"])
fetch_docs_from_chunk(path)
_ ->
{:error, :module_not_found}
end
end
end
def fetch_docs(path) when is_binary(path) do
do_fetch_docs(String.to_charlist(path))
fetch_docs_from_beam(String.to_charlist(path))
end
@docs_chunk 'Docs'
defp do_fetch_docs(bin_or_path) do
defp fetch_docs_from_beam(bin_or_path) do
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
{:ok, {_module, [{@docs_chunk, bin}]}} ->
try do
:erlang.binary_to_term(bin)
rescue
_ -> {:error, {:invalid_chunk, bin}}
end
load_docs_chunk(bin)
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} ->
{:error, :chunk_not_found}
{:error, :beam_lib, {:file_error, _, :enoent}} ->
{:error, :module_not_found}
end
end
defp fetch_docs_from_chunk(path) do
case File.read(path) do
{:ok, bin} ->
load_docs_chunk(bin)
{:error, _} ->
{:error, :chunk_not_found}
end
end
defp load_docs_chunk(bin) do
:erlang.binary_to_term(bin)
rescue
_ ->
{:error, {:invalid_chunk, bin}}
end
@doc ~S"""
Deprecated function to retrieve old documentation format.
@@ -1392,6 +1456,6 @@ defmodule Code do
defp verify_loaded(loaded) do
maps_binaries = Enum.map(loaded, fn {_module, map, binary} -> {map, binary} end)
Module.ParallelChecker.verify(maps_binaries, [])
Enum.map(loaded, fn {module, map, _binary} -> {module, map} end)
Enum.map(loaded, fn {module, _map, binary} -> {module, binary} end)
end
end
+25 -11
View File
@@ -42,6 +42,8 @@ defmodule Code.Formatter do
:<~>,
:<|>,
:^^^,
:+++,
:---,
:in,
:++,
:--,
@@ -140,8 +142,8 @@ defmodule Code.Formatter do
Checks if two strings are equivalent.
"""
def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, "nofile", [])
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, "nofile", [])
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, 1, "nofile", [])
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, 1, "nofile", [])
case not_equivalent(quoted1, quoted2) do
{left, right} -> {:error, left, right}
@@ -210,7 +212,7 @@ defmodule Code.Formatter do
token_metadata: true
]
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, file, tokenizer_options),
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, 1, file, tokenizer_options),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, parser_options) do
state =
Process.get(:code_formatter_comments)
@@ -237,8 +239,8 @@ defmodule Code.Formatter do
{:ok, doc} ->
doc
{:error, {line, error, token}} ->
:elixir_errors.parse_error(line, Keyword.get(opts, :file, "nofile"), error, token)
{:error, {location, error, token}} ->
:elixir_errors.parse_error(location, Keyword.get(opts, :file, "nofile"), error, token)
end
end
@@ -483,6 +485,10 @@ defmodule Code.Formatter do
quoted_to_algebra(arg, context, state)
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{"nil", state}
end
defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do
{block, state} = block_to_algebra(block, line(meta), closing_line(meta), state)
{surround("(", block, ")"), state}
@@ -1914,8 +1920,7 @@ defmodule Code.Formatter do
{args_docs, comments?, %{state | comments: comments}}
end
defp each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun) do
[arg | args] = args
defp each_quoted_to_algebra_with_comments([arg | args], acc, max_line, state, comments?, fun) do
{doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line})
{acc, comments, comments?} =
@@ -1926,7 +1931,7 @@ defmodule Code.Formatter do
{acc, comments, comments?} =
extract_comments_trailing(doc_start, doc_end, acc, state.comments, comments?)
acc = [doc_triplet | acc]
acc = [adjust_trailing_newlines(doc_triplet, doc_end, comments) | acc]
state = %{state | comments: comments}
each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun)
end
@@ -1957,6 +1962,15 @@ defmodule Code.Formatter do
{acc, rest, comments?}
end
# If the document is immediately followed by comment which is followed by newlines,
# its newlines wouldn't have considered the comment, so we need to adjust it.
defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [{line, _, _} | _])
when newlines > 1 and line == doc_end + 1 do
{doc, next_line, 1}
end
defp adjust_trailing_newlines(doc_triplet, _, _), do: doc_triplet
defp traverse_line({expr, meta, args}, {min, max}) do
acc =
case Keyword.fetch(meta, :line) do
@@ -2139,7 +2153,7 @@ defmodule Code.Formatter do
end
defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do
meta[:delimeter] != ~s[']
meta[:delimiter] != ~s[']
end
defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do
@@ -2239,10 +2253,10 @@ defmodule Code.Formatter do
defp keyword?(rest), do: rest == []
defp keyword_key?({:__block__, meta, [atom]}) when is_atom(atom),
do: meta[:delimiter] != ":"
do: meta[:format] == :keyword
defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, meta, [{:<<>>, _, _}, :utf8]}),
do: meta[:delimiter] != ":"
do: meta[:format] == :keyword
defp keyword_key?(_),
do: false
+1 -1
View File
@@ -44,7 +44,7 @@ defmodule Code.Identifier do
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 170}
op in [:in] -> {:left, 180}
op in [:^^^] -> {:left, 190}
op in [:++, :--, :.., :<>] -> {:right, 200}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
op in [:.] -> {:left, 310}
+8 -7
View File
@@ -217,7 +217,8 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:user_type, line, name, args}) do
typespec_to_quoted({:type, line, name, args})
args = for arg <- args, do: typespec_to_quoted(arg)
{name, [line: line], args}
end
defp typespec_to_quoted({:type, line, :tuple, :any}) do
@@ -272,13 +273,13 @@ defmodule Code.Typespec do
{{:optional, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
end)
{struct, fields} = Keyword.pop(fields, :__struct__)
map = {:%{}, [line: line], fields}
case List.keytake(fields, :__struct__, 0) do
{{:__struct__, struct}, fields_pruned} when is_atom(struct) and struct != nil ->
map_pruned = {:%{}, [line: line], fields_pruned}
{:%, [line: line], [struct, map_pruned]}
if struct do
{:%, [line: line], [struct, map]}
else
map
_ ->
{:%{}, [line: line], fields}
end
end
+115 -53
View File
@@ -13,7 +13,7 @@ defmodule Config do
key1: "value1",
key2: "value2"
import_config "#{Mix.env()}.exs"
import_config "#{config_env()}.exs"
`import Config` will import the functions `config/2`, `config/3`
and `import_config/1` to help you manage your configuration.
@@ -26,9 +26,9 @@ defmodule Config do
"value1" = Application.fetch_env!(:some_app, :key1)
Finally, the line `import_config "#{Mix.env()}.exs"` will import other
config files, based on the current Mix environment, such as
`config/dev.exs` and `config/test.exs`.
Finally, the line `import_config "#{config_env()}.exs"` will import
other config files based on the current configuration environment,
such as `config/dev.exs` and `config/test.exs`.
`Config` also provides a low-level API for evaluating and reading
configuration, under the `Config.Reader` module.
@@ -36,14 +36,14 @@ defmodule Config do
**Important:** if you are writing a library to be used by other developers,
it is generally recommended to avoid the application environment, as the
application environment is effectively a global storage. For more information,
read our [library guidelines](library-guidelines.html).
read our [library guidelines](library-guidelines.md).
## Migrating from `use Mix.Config`
The `Config` module in Elixir was introduced in v1.9 as a replacement to
`Mix.Config`, which was specific to Mix and has been deprecated.
You can leverage `Config` instead of `Mix.Config` in two steps. The first
You can leverage `Config` instead of `Mix.Config` in three steps. The first
step is to replace `use Mix.Config` at the top of your config files by
`import Config`.
@@ -59,43 +59,30 @@ defmodule Config do
import_config config
end
## config/releases.exs
The last step is to replace all `Mix.env()` calls by `config_env()`.
If you are using releases, see `mix release`, there is another configuration
file called `config/releases.exs`. While `config/config.exs` and friends
mentioned in the previous section are executed whenever you run a Mix
command, including when you assemble a release, `config/releases.exs` is
executed every time your production system boots. Since Mix is not available
in a production system, `config/releases.exs` must not use any of the
functions from Mix.
## config/runtime.exs
For runtime configuration, you can use the `config/runtime.exs` file.
It is executed right before applications start in both Mix and releases
(assembled with `mix release`).
"""
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@files_key {__MODULE__, :files}
@imports_key {__MODULE__, :imports}
defp get_config!() do
Process.get(@config_key) || raise_improper_use!()
end
defp get_opts!(), do: Process.get(@opts_key)
defp put_opts(value), do: Process.put(@opts_key, value)
defp delete_opts(), do: Process.delete(@opts_key)
defp put_config(value) do
Process.put(@config_key, value)
end
defp get_config!(), do: Process.get(@config_key) || raise_improper_use!()
defp put_config(value), do: Process.put(@config_key, value)
defp delete_config(), do: Process.delete(@config_key)
defp delete_config() do
Process.delete(@config_key)
end
defp get_files!() do
Process.get(@files_key) || raise_improper_use!()
end
defp put_files(value) do
Process.put(@files_key, value)
end
defp delete_files() do
Process.delete(@files_key)
end
defp get_imports!(), do: Process.get(@imports_key) || raise_improper_use!()
defp put_imports(value), do: Process.put(@imports_key, value)
defp delete_imports(), do: Process.delete(@imports_key)
defp raise_improper_use!() do
raise "could not set configuration via Config. " <>
@@ -172,6 +159,55 @@ defmodule Config do
|> put_config()
end
@doc """
Returns the environment this configuration file is executed on.
In Mix projects this function returns the environment this configuration
file is executed on. In releases, the environment when `mix release` ran.
This is most often used to execute conditional code:
if config_env() == :prod do
config :my_app, :debug, false
end
"""
@doc since: "1.11.0"
defmacro config_env() do
quote do
Config.__env__!()
end
end
@doc false
@spec __env__!() :: atom()
def __env__!() do
elem(get_opts!(), 0) || raise "no :env key was given to this configuration file"
end
@doc """
Returns the target this configuration file is executed on.
This is most often used to execute conditional code:
if config_target() == :host do
config :my_app, :debug, false
end
"""
@doc since: "1.11.0"
defmacro config_target() do
quote do
Config.__target__!()
end
end
@doc false
@spec __target__!() :: atom()
def __target__!() do
elem(get_opts!(), 1) || raise "no :target key was given to this configuration file"
end
@doc ~S"""
Imports configuration from the given file.
@@ -184,8 +220,11 @@ defmodule Config do
This is often used to emulate configuration across environments:
import_config "#{Mix.env()}.exs"
import_config "#{config_env()}.exs"
Note, however, some configuration files, such as `config/runtime.exs`
does not support imports, as they are meant to be copied across
systems.
"""
@doc since: "1.9.0"
defmacro import_config(file) do
@@ -198,39 +237,62 @@ defmodule Config do
@doc false
@spec __import__!(Path.t()) :: {term, Code.binding()}
def __import__!(file) when is_binary(file) do
current_files = get_files!()
if file in current_files do
raise ArgumentError,
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
end
put_files([file | current_files])
Code.eval_file(file)
import_config!(file, File.read!(file), true)
end
@doc false
@spec __eval__!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
def __eval__!(file, imported_paths \\ []) when is_binary(file) and is_list(imported_paths) do
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target)
imports = Keyword.get(opts, :imports, [])
previous_opts = put_opts({env, target})
previous_config = put_config([])
previous_files = put_files(imported_paths)
previous_imports = put_imports(imports)
try do
{eval_config, _} = __import__!(Path.expand(file))
{eval_config, _} = import_config!(file, content, false)
case get_config!() do
[] when is_list(eval_config) ->
{validate!(eval_config, file), get_files!()}
{validate!(eval_config, file), get_imports!()}
pdict_config ->
{pdict_config, get_files!()}
{pdict_config, get_imports!()}
end
after
if previous_opts, do: put_opts(previous_opts), else: delete_opts()
if previous_config, do: put_config(previous_config), else: delete_config()
if previous_files, do: put_files(previous_files), else: delete_files()
if previous_imports, do: put_imports(previous_imports), else: delete_imports()
end
end
defp import_config!(file, contents, raise_when_disabled?) do
current_imports = get_imports!()
cond do
current_imports == :disabled ->
if raise_when_disabled? do
raise "import_config/1 is not enabled for this configuration file. " <>
"Some configuration files do not allow importing other files " <>
"as they are often copied to external systems"
end
file in current_imports ->
raise ArgumentError,
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
true ->
put_imports([file | current_imports])
:ok
end
# TODO: Emit a warning if Mix.env() is found in said files in Elixir v1.15.
# Note this won't be a deprecation warning as it will always be emitted.
Code.eval_string(contents, [], file: file)
end
@doc false
def __merge__(config1, config2) when is_list(config1) and is_list(config2) do
Keyword.merge(config1, config2, fn _, app1, app2 ->
+92 -58
View File
@@ -109,10 +109,13 @@ defmodule Config.Provider do
:providers,
:config_path,
extra_config: [],
prune_after_boot: false,
reboot_after_config: true
prune_runtime_sys_config_after_boot: false,
reboot_system_after_config: false,
validate_compile_env: false
]
@reserved_apps [:kernel, :stdlib]
@doc """
Validates a `t:config_path/0`.
"""
@@ -146,80 +149,99 @@ defmodule Config.Provider do
def resolve_config_path!(path) when is_binary(path), do: path
def resolve_config_path!({:system, name, path}), do: System.fetch_env!(name) <> path
# Private keys
@init_key :config_provider_init
@booted_key :config_provider_booted
# Public keys
@reboot_mode_key :config_provider_reboot_mode
@doc false
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
init = struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
[elixir: [{@init_key, init}]]
end
@doc false
def boot(app, key, restart_fun \\ &restart_and_sleep/0) do
# The app with the config provider settings may not
# have been loaded at this point, so make sure we load
# its environment before querying it.
_ = :application.load(app)
def boot(reboot_fun \\ &restart_and_sleep/0) do
# The config provider typically runs very early in the
# release process, so we need to make sure Elixir is started
# before we go around running Elixir code.
{:ok, _} = :application.ensure_all_started(:elixir)
case :application.get_env(app, key) do
{:ok, %Config.Provider{} = provider} ->
path = resolve_config_path!(provider.config_path)
validate_no_cyclic_boot!(path)
loaded_applications = :application.loaded_applications()
original_config = read_config!(path)
case Application.fetch_env(:elixir, @booted_key) do
{:ok, {:booted, path}} ->
path && File.rm(path)
config =
original_config
|> Config.__merge__(provider.extra_config)
|> run_providers(provider)
if provider.reboot_after_config do
config
|> Config.__merge__([{app, [{key, booted_key(provider, path)}]}])
|> write_config!(path)
restart_fun.()
else
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
abort("""
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
to false and #{inspect(app)} has already been loaded, meaning any further \
configuration won't have an effect.
The configuration for #{inspect(app)} before config providers was:
#{inspect(original_config[app])}
The configuration for #{inspect(app)} after config providers was:
#{inspect(config[app])}
""")
end
_ = Application.put_all_env(config, persistent: true)
:ok
with {:ok, %Config.Provider{} = provider} <- Application.fetch_env(:elixir, @init_key) do
maybe_validate_compile_env(provider)
end
{:ok, {:booted, path}} ->
File.rm(path)
:booted
{:ok, :booted} ->
:booted
_ ->
:skip
case Application.fetch_env(:elixir, @init_key) do
{:ok, %Config.Provider{} = provider} ->
path = resolve_config_path!(provider.config_path)
reboot_config = [elixir: [{@booted_key, booted_value(provider, path)}]]
boot_providers(path, provider, reboot_config, reboot_fun)
_ ->
:skip
end
end
end
defp boot_providers(path, provider, reboot_config, reboot_fun) do
validate_no_cyclic_boot!(path)
original_config = read_config!(path)
config =
original_config
|> Config.__merge__(provider.extra_config)
|> run_providers(provider)
if provider.reboot_system_after_config do
config
|> Config.__merge__(reboot_config)
|> write_config!(path)
reboot_fun.()
else
for app <- @reserved_apps, config[app] != original_config[app] do
abort("""
Cannot configure #{inspect(app)} because :reboot_system_after_config has been set \
to false and #{inspect(app)} has already been loaded, meaning any further \
configuration won't have an effect.
The configuration for #{inspect(app)} before config providers was:
#{inspect(original_config[app])}
The configuration for #{inspect(app)} after config providers was:
#{inspect(config[app])}
""")
end
_ = Application.put_all_env(config, persistent: true)
maybe_validate_compile_env(provider)
:ok
end
end
defp maybe_validate_compile_env(provider) do
with [_ | _] = compile_env <- provider.validate_compile_env do
validate_compile_env(compile_env)
end
end
@doc false
def validate_compile_env(compile_env) do
def validate_compile_env(compile_env, ensure_loaded? \\ true) do
for {app, [key | path], compile_return} <- compile_env,
Application.ensure_loaded(app) == :ok do
ensure_app_loaded?(app, ensure_loaded?) do
try do
traverse_env(Application.fetch_env(app, key), path)
rescue
@@ -254,6 +276,9 @@ defmodule Config.Provider do
:ok
end
defp ensure_app_loaded?(app, true), do: Application.ensure_loaded(app) == :ok
defp ensure_app_loaded?(app, false), do: Application.spec(app, :vsn) != nil
defp path(key, []), do: "for key #{inspect(key)}"
defp path(key, path), do: "for path #{inspect(path)} inside key #{inspect(key)}"
@@ -278,13 +303,22 @@ defmodule Config.Provider do
defp traverse_env(:error, _paths), do: :error
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
defp restart_and_sleep do
:init.restart()
@compile {:no_warn_undefined, {:init, :restart, 1}}
defp restart_and_sleep() do
mode = Application.get_env(:elixir, @reboot_mode_key)
# TODO: Remove otp_release check once we require Erlang/OTP 23+
if :erlang.system_info(:otp_release) >= '23' and mode in [:embedded, :interactive] do
:init.restart(mode: mode)
else
:init.restart()
end
Process.sleep(:infinity)
end
defp booted_key(%{prune_after_boot: true}, path), do: {:booted, path}
defp booted_key(%{prune_after_boot: false}, _path), do: :booted
defp booted_value(%{prune_runtime_sys_config_after_boot: true}, path), do: {:booted, path}
defp booted_value(%{prune_runtime_sys_config_after_boot: false}, _path), do: {:booted, nil}
defp validate_no_cyclic_boot!(path) do
if System.get_env("ELIXIR_CONFIG_PROVIDER_BOOTED") do
@@ -329,7 +363,7 @@ defmodule Config.Provider do
defp write_config!(config, path) do
contents = :io_lib.format("%% coding: utf-8~n~tw.~n", [config])
case File.write(path, contents, [:utf8]) do
case File.write(path, IO.chardata_to_string(contents)) do
:ok ->
:ok
+56 -24
View File
@@ -18,8 +18,18 @@ defmodule Config.Reader do
config_provider: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
You can also pass a keyword list of options to the reader,
where the `:path` is a required key:
config_providers: [
{Config.Reader,
path: "/etc/config.exs",
env: :prod,
imports: :disabled}
]
Note by default Mix releases supports runtime configuration via
a `config/releases.exs`. If a `config/releases.exs` exists in your
a `config/runtime.exs`. If a `config/runtime.exs` exists in your
application, it is automatically copied inside the release and
automatically set as a config provider.
"""
@@ -27,49 +37,71 @@ defmodule Config.Reader do
@behaviour Config.Provider
@impl true
def init(path) do
def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path)
Config.Provider.validate_config_path!(path)
path
{path, opts}
end
def init(path) do
init(path: path)
end
@impl true
def load(config, path) do
merge(config, path |> Config.Provider.resolve_config_path!() |> read!())
def load(config, {path, opts}) do
merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
end
@doc """
Evaluates the configuration `contents` for the given `file`.
Accepts the same options as `read!/2`.
"""
@doc since: "1.11.0"
@spec eval!(Path.t(), binary, keyword) :: keyword
def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
end
@doc """
Reads the configuration file.
The same as `read_imports!/2` but only returns the configuration
in the given file, without returning the imported paths.
## Options
It exists for convenience purposes. For example, you could
invoke it inside your `mix.exs` to read some external data
you decided to move to a configuration file:
* `:imports` - a list of already imported paths or `:disabled`
to disable imports
releases: Config.Reader.read!("rel/releases.exs")
* `:env` - the environment the configuration file runs on.
See `Config.config_env/0` for sample usage
* `:target` - the target the configuration file runs on.
See `Config.config_target/0` for sample usage
"""
@doc since: "1.9.0"
@spec read!(Path.t(), [Path.t()]) :: keyword
def read!(file, imported_paths \\ [])
when is_binary(file) and is_list(imported_paths) do
Config.__eval__!(file, imported_paths) |> elem(0)
@spec read!(Path.t(), keyword) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
end
@doc """
Reads the given configuration file alongside its imports.
Reads the given configuration file and returns the configuration
with its imports.
It accepts a list of `imported_paths` that should raise if attempted
to be imported again (to avoid recursive imports).
It returns a tuple with the configuration and the imported paths.
Accepts the same options as `read!/2`. Although note the `:imports`
option cannot be disabled in `read_imports!/2`.
"""
@doc since: "1.9.0"
@spec read_imports!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
def read_imports!(file, imported_paths \\ [])
when is_binary(file) and is_list(imported_paths) do
Config.__eval__!(file, imported_paths)
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths"
end
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts)
end
@doc """
+8 -5
View File
@@ -18,11 +18,14 @@ defmodule Dict do
message =
"Use the Map module for working with maps or the Keyword module for working with keyword lists"
@deprecated message
defmacro __using__(_) do
# Use this import to guarantee proper code expansion
import Kernel, except: [size: 1]
if __CALLER__.module != HashDict do
IO.warn("use Dict is deprecated. " <> unquote(message), Macro.Env.stacktrace(__CALLER__))
end
quote do
message = "Use maps and the Map module instead"
@@ -152,13 +155,13 @@ defmodule Dict do
end
@deprecated message
def update(dict, key, initial, fun) do
def update(dict, key, default, fun) do
case fetch(dict, key) do
{:ok, value} ->
put(dict, key, fun.(value))
:error ->
put(dict, key, initial)
put(dict, key, default)
end
end
@@ -375,8 +378,8 @@ defmodule Dict do
@deprecated message
@spec update(t, key, value, (value -> value)) :: t
def update(dict, key, initial, fun) do
target(dict).update(dict, key, initial, fun)
def update(dict, key, default, fun) do
target(dict).update(dict, key, default, fun)
end
@deprecated message
+2 -1
View File
@@ -418,7 +418,8 @@ defmodule DynamicSupervisor do
"""
@doc since: "1.6.0"
@spec which_children(Supervisor.supervisor()) :: [
{:undefined, pid | :restarting, :worker | :supervisor, :supervisor.modules()}
# module() | :dynamic here because :supervisor.modules() is not exported
{:undefined, pid | :restarting, :worker | :supervisor, module() | :dynamic}
]
def which_children(supervisor) do
call(supervisor, :which_children)
+152 -65
View File
@@ -20,7 +20,7 @@ defprotocol Enumerable do
Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
end
Notice the user-supplied function is wrapped into a `t:reducer/0` function.
Note that the user-supplied function is wrapped into a `t:reducer/0` function.
The `t:reducer/0` function must return a tagged tuple after each step,
as described in the `t:acc/0` type. At the end, `Enumerable.reduce/3`
returns `t:result/0`.
@@ -161,6 +161,9 @@ defprotocol Enumerable do
Otherwise it should return `{:error, __MODULE__}` and a default algorithm
built on top of `reduce/3` that runs in linear time will be used.
When not called whithin guards, the [`in`](`in/2`) and [`not in`](`in/2`)
operators work by using this function.
"""
@spec member?(t, term) :: {:ok, boolean} | {:error, module}
def member?(enumerable, element)
@@ -1334,12 +1337,12 @@ defmodule Enum do
end
@doc """
Joins the given `enumerable` into a binary using `joiner` as a
Joins the given `enumerable` into a string using `joiner` as a
separator.
If `joiner` is not passed at all, it defaults to the empty binary.
If `joiner` is not passed at all, it defaults to an empty string.
All elements in the `enumerable` must be convertible to a binary,
All elements in the `enumerable` must be convertible to a string,
otherwise an error is raised.
## Examples
@@ -1473,12 +1476,10 @@ defmodule Enum do
@doc """
Maps and joins the given `enumerable` in one pass.
`joiner` can be either a binary or a list and the result will be of
the same type as `joiner`.
If `joiner` is not passed at all, it defaults to an empty binary.
If `joiner` is not passed at all, it defaults to an empty string.
All elements returned from invoking the `mapper` must be convertible to
a binary, otherwise an error is raised.
a string, otherwise an error is raised.
## Examples
@@ -1530,6 +1531,12 @@ defmodule Enum do
{:lists.reverse(list), acc}
end
@doc false
@spec max(t, (() -> empty_result)) :: element | empty_result when empty_result: any
def max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
max(enumerable, &>=/2, empty_fallback)
end
@doc """
Returns the maximal element in the `enumerable` according
to Erlang's term ordering.
@@ -1557,8 +1564,8 @@ defmodule Enum do
In the example above, `max/2` returned March 31st instead of April 1st
because the structural comparison compares the day before the year.
For this reason, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1572,26 +1579,30 @@ defmodule Enum do
0
"""
@spec max(t, (element, element -> boolean) | module()) ::
element | empty_result
when empty_result: any
@spec max(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
element | empty_result
when empty_result: any
def max(enumerable, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end) do
{sorter, empty_fallback} = max_sort_fun(sorter, empty_fallback)
aggregate(enumerable, sorter, empty_fallback)
aggregate(enumerable, max_sort_fun(sorter), empty_fallback)
end
# TODO: Deprecate me on 1.14
defp max_sort_fun(empty_fallback, default_empty_fallback)
when is_function(empty_fallback, 0) and is_function(default_empty_fallback, 0),
do: {&>=/2, empty_fallback}
defp max_sort_fun(sorter) when is_function(sorter, 2), do: sorter
defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)
defp max_sort_fun(sorter, empty_fallback)
when is_function(sorter, 2) and is_function(empty_fallback, 0),
do: {sorter, empty_fallback}
defp max_sort_fun(module, empty_fallback)
when is_atom(module) and is_function(empty_fallback, 0),
do: {&(module.compare(&1, &2) != :lt), empty_fallback}
@doc false
@spec max_by(
t,
(element -> any),
(() -> empty_result) | (element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
def max_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
max_by(enumerable, fun, &>=/2, empty_fallback)
end
@doc """
Returns the maximal element in the `enumerable` as calculated
@@ -1617,8 +1628,8 @@ defmodule Enum do
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1646,8 +1657,7 @@ defmodule Enum do
when empty_result: any
def max_by(enumerable, fun, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
when is_function(fun, 1) do
{sorter, empty_fallback} = max_sort_fun(sorter, empty_fallback)
aggregate_by(enumerable, fun, sorter, empty_fallback)
aggregate_by(enumerable, fun, max_sort_fun(sorter), empty_fallback)
end
@doc """
@@ -1670,6 +1680,9 @@ defmodule Enum do
iex> Enum.member?([:a, :b, :c], :d)
false
When not called whithin guards, the [`in`](`in/2`) and [`not in`](`in/2`)
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) when is_list(enumerable) do
@@ -1690,6 +1703,12 @@ defmodule Enum do
end
end
@doc false
@spec min(t, (() -> empty_result)) :: element | empty_result when empty_result: any
def min(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
min(enumerable, &<=/2, empty_fallback)
end
@doc """
Returns the minimal element in the `enumerable` according
to Erlang's term ordering.
@@ -1717,8 +1736,8 @@ defmodule Enum do
In the example above, `min/2` returned April 1st instead of March 31st
because the structural comparison compares the day before the year.
For this reason, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1728,30 +1747,34 @@ defmodule Enum do
Finally, if you don't want to raise on empty enumerables, you can pass
the empty fallback:
iex> Enum.min([], &<=/2, fn -> 0 end)
iex> Enum.min([], fn -> 0 end)
0
"""
@spec min(t, (element, element -> boolean) | module()) ::
element | empty_result
when empty_result: any
@spec min(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
element | empty_result
when empty_result: any
def min(enumerable, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end) do
{sorter, empty_fallback} = min_sort_fun(sorter, empty_fallback)
aggregate(enumerable, sorter, empty_fallback)
aggregate(enumerable, min_sort_fun(sorter), empty_fallback)
end
# TODO: Deprecate me on 1.14
defp min_sort_fun(empty_fallback, default_empty_fallback)
when is_function(empty_fallback, 0) and is_function(default_empty_fallback, 0),
do: {&<=/2, empty_fallback}
defp min_sort_fun(sorter) when is_function(sorter, 2), do: sorter
defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)
defp min_sort_fun(sorter, empty_fallback)
when is_function(sorter, 2) and is_function(empty_fallback, 0),
do: {sorter, empty_fallback}
defp min_sort_fun(module, empty_fallback)
when is_atom(module) and is_function(empty_fallback, 0),
do: {&(module.compare(&1, &2) != :gt), empty_fallback}
@doc false
@spec min_by(
t,
(element -> any),
(() -> empty_result) | (element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
def min_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
min_by(enumerable, fun, &<=/2, empty_fallback)
end
@doc """
Returns the minimal element in the `enumerable` as calculated
@@ -1777,8 +1800,8 @@ defmodule Enum do
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -1797,12 +1820,16 @@ defmodule Enum do
nil
"""
@spec min_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
@spec min_by(
t,
(element -> any),
(element, element -> boolean) | module(),
(() -> empty_result)
) :: element | empty_result
when empty_result: any
def min_by(enumerable, fun, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
when is_function(fun, 1) do
{sorter, empty_fallback} = min_sort_fun(sorter, empty_fallback)
aggregate_by(enumerable, fun, sorter, empty_fallback)
aggregate_by(enumerable, fun, min_sort_fun(sorter), empty_fallback)
end
@doc """
@@ -1845,6 +1872,14 @@ defmodule Enum do
end
end
@doc false
@spec min_max_by(t, (element -> any), (() -> empty_result)) :: {element, element} | empty_result
when empty_result: any
def min_max_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, &</2, empty_fallback)
end
@doc """
Returns a tuple with the minimal and the maximal elements in the
enumerable as calculated by the given function.
@@ -1852,9 +1887,6 @@ defmodule Enum do
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
Calls the provided `empty_fallback` function and returns its value if
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
## Examples
iex> Enum.min_max_by(["aaa", "bb", "c"], fn x -> String.length(x) end)
@@ -1866,11 +1898,56 @@ defmodule Enum do
iex> Enum.min_max_by([], &String.length/1, fn -> {nil, nil} end)
{nil, nil}
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
iex> users = [
...> %{name: "Ellis", birthday: ~D[1943-05-11]},
...> %{name: "Lovelace", birthday: ~D[1815-12-10]},
...> %{name: "Turing", birthday: ~D[1912-06-23]}
...> ]
iex> Enum.min_max_by(users, &(&1.birthday), Date)
{
%{name: "Lovelace", birthday: ~D[1815-12-10]},
%{name: "Ellis", birthday: ~D[1943-05-11]}
}
Finally, if you don't want to raise on empty enumerables, you can pass
the empty fallback:
iex> Enum.min_max_by([], &String.length/1, fn -> nil end)
nil
"""
@spec min_max_by(t, (element -> any), (() -> empty_result)) :: {element, element} | empty_result
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
{element, element} | empty_result
when empty_result: any
def min_max_by(enumerable, fun, empty_fallback \\ fn -> raise Enum.EmptyError end)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
@spec min_max_by(
t,
(element -> any),
(element, element -> boolean) | module(),
(() -> empty_result)
) :: {element, element} | empty_result
when empty_result: any
def min_max_by(
enumerable,
fun,
sorter_or_empty_fallback \\ &</2,
empty_fallback \\ fn -> raise Enum.EmptyError end
)
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
end
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_function(sorter, 2) and is_function(empty_fallback, 0) do
first_fun = fn entry ->
fun_entry = fun.(entry)
{entry, entry, fun_entry, fun_entry}
@@ -1880,10 +1957,10 @@ defmodule Enum do
fun_entry = fun.(entry)
cond do
fun_entry < fun_min ->
sorter.(fun_entry, fun_min) ->
{entry, prev_max, fun_entry, fun_max}
fun_entry > fun_max ->
sorter.(fun_max, fun_entry) ->
{prev_min, entry, fun_min, fun_entry}
true ->
@@ -1897,6 +1974,8 @@ defmodule Enum do
end
end
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
@doc """
Splits the `enumerable` in two lists according to the given function `fun`.
@@ -2097,7 +2176,7 @@ defmodule Enum do
operation cannot be expressed by any of the functions in the `Enum`
module, developers will most likely resort to `reduce/3`.
"""
@spec reduce(t, any, (element, any -> any)) :: any
@spec reduce(t, any, (element, acc -> acc)) :: acc
def reduce(enumerable, acc, fun) when is_list(enumerable) do
:lists.foldl(fun, acc, enumerable)
end
@@ -2485,15 +2564,15 @@ defmodule Enum do
iex> Enum.sort(dates)
[~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]
Notice the returned result is incorrect, because `sort/1` by default uses
Note that the returned result is incorrect, because `sort/1` by default uses
`<=/2`, which will compare their structure. When comparing structures, the
fields are compared in alphabetical order, which means the dates above will
be compared by `day`, `month` and then `year`, which is the opposite of what
we want.
For this reason, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
`:eq` (equal), and `:gt` (greather than). If you pass a module as the
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
@@ -2829,7 +2908,7 @@ defmodule Enum do
@doc """
Takes `count` random elements from `enumerable`.
Notice this function will traverse the whole `enumerable` to
Note that this function will traverse the whole `enumerable` to
get the random sublist.
See `random/1` for notes on implementation and random seed.
@@ -3071,10 +3150,18 @@ defmodule Enum do
"""
@spec with_index(t, integer) :: [{element, index}]
def with_index(enumerable, offset \\ 0) do
map_reduce(enumerable, offset, fn x, acc ->
{{x, acc}, acc + 1}
end)
|> elem(0)
enumerable
|> to_list()
|> do_with_index(offset)
end
@spec do_with_index(list, integer) :: [{element, index}]
defp do_with_index([], _) do
[]
end
defp do_with_index([head | tail], index) do
[{head, index} | do_with_index(tail, index + 1)]
end
@doc """
+74 -47
View File
@@ -21,15 +21,15 @@ defmodule Exception do
@typedoc "The kind handled by formatting functions"
@type kind :: :error | non_error_kind
@typep non_error_kind :: :exit | :throw | {:EXIT, pid}
@type non_error_kind :: :exit | :throw | {:EXIT, pid}
@type stacktrace :: [stacktrace_entry]
@type stacktrace_entry ::
{module, atom, arity_or_args, location}
| {(... -> any), arity_or_args, location}
@typep arity_or_args :: non_neg_integer | list
@typep location :: keyword
@type arity_or_args :: non_neg_integer | list
@type location :: keyword
@callback exception(term) :: t
@callback message(t) :: String.t()
@@ -46,6 +46,8 @@ defmodule Exception do
@doc """
Returns `true` if the given `term` is an exception.
"""
# TODO: Remove this on Elixir v1.15
@doc deprecated: "Use Kernel.is_exception/1 instead"
def exception?(term)
def exception?(%_{__exception__: true}), do: true
def exception?(_), do: false
@@ -188,7 +190,7 @@ defmodule Exception do
Where `definition` is `:def`, `:defp`, `:defmacro` or `:defmacrop`.
"""
@doc since: "1.5.0"
@spec blame_mfa(module, function, args :: [term]) ::
@spec blame_mfa(module, function :: atom, args :: [term]) ::
{:ok, :def | :defp | :defmacro | :defmacrop, [{args :: [term], guards :: [term]}]}
| :error
def blame_mfa(module, function, args)
@@ -237,7 +239,11 @@ defmodule Exception do
binding = :orddict.store(:VAR, call_arg, binding)
try do
{:value, _, binding} = :erl_eval.expr({:match, 0, erl_arg, {:var, 0, :VAR}}, binding, :none)
ann = :erl_anno.new(0)
{:value, _, binding} =
:erl_eval.expr({:match, ann, erl_arg, {:var, ann, :VAR}}, binding, :none)
{true, binding}
rescue
_ -> {false, binding}
@@ -261,7 +267,13 @@ defmodule Exception do
blame_guard(right, scope, binding)
]
{rewrite_guard_call(op), meta, guards}
kernel_op =
case op do
:orelse -> :or
:andalso -> :and
end
{kernel_op, meta, guards}
end
defp blame_guard(ex_guard, scope, binding) do
@@ -280,32 +292,17 @@ defmodule Exception do
defp rewrite_guard(guard) do
Macro.prewalk(guard, fn
{{:., _, [:erlang, :element]}, _, [{{:., _, [:erlang, :+]}, _, [int, 1]}, arg]} ->
{:elem, [], [arg, int]}
{{:., _, [:erlang, :element]}, _, [int, arg]} when is_integer(int) ->
{:elem, [], [arg, int - 1]}
{:., _, [:erlang, call]} ->
rewrite_guard_call(call)
other ->
other
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp rewrite_guard_call(:orelse), do: :or
defp rewrite_guard_call(:andalso), do: :and
defp rewrite_guard_call(:"=<"), do: :<=
defp rewrite_guard_call(:"/="), do: :!=
defp rewrite_guard_call(:"=:="), do: :===
defp rewrite_guard_call(:"=/="), do: :!==
defp rewrite_guard_call(op) when op in [:band, :bor, :bnot, :bsl, :bsr, :bxor],
do: {:., [], [Bitwise, op]}
defp rewrite_guard_call(op) when op in [:xor, :element, :size], do: {:., [], [:erlang, op]}
defp rewrite_guard_call(op), do: op
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
defp blame_wrap(match?, ast), do: %{match?: match?, node: ast}
@@ -638,6 +635,7 @@ defmodule Exception do
@doc """
Formats the given `file` and `line` as shown in stacktraces.
If any of the values are `nil`, they are omitted.
## Examples
@@ -653,14 +651,42 @@ defmodule Exception do
"""
def format_file_line(file, line, suffix \\ "") do
if file do
if line && line != 0 do
"#{file}:#{line}:#{suffix}"
else
"#{file}:#{suffix}"
end
else
cond do
is_nil(file) -> ""
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
true -> "#{file}:#{line}:#{suffix}"
end
end
@doc """
Formats the given `file`, `line`, and `column` as shown in stacktraces.
If any of the values are `nil`, they are omitted.
## Examples
iex> Exception.format_file_line_column("foo", 1, 2)
"foo:1:2:"
iex> Exception.format_file_line_column("foo", 1, nil)
"foo:1:"
iex> Exception.format_file_line_column("foo", nil, nil)
"foo:"
iex> Exception.format_file_line_column("foo", nil, 2)
"foo:"
iex> Exception.format_file_line_column(nil, nil, nil)
""
"""
def format_file_line_column(file, line, column, suffix \\ "") do
cond do
is_nil(file) -> ""
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
is_nil(column) or column == 0 -> "#{file}:#{line}:#{suffix}"
true -> "#{file}:#{line}:#{column}:#{suffix}"
end
end
@@ -692,7 +718,7 @@ defmodule ArgumentError do
not is_atom(module) and is_atom(function) and args == [] ->
"you attempted to apply #{inspect(function)} on #{inspect(module)}. " <>
"If you are using apply/3, make sure the module is an atom. " <>
"If you are using the dot syntax, such as map.field or module.function, " <>
"If you are using the dot syntax, such as map.field or module.function(), " <>
"make sure the left side of the dot is an atom or a map"
not is_atom(module) ->
@@ -766,21 +792,22 @@ defmodule SystemLimitError do
end
defmodule SyntaxError do
defexception [:file, :line, description: "syntax error"]
defexception [:file, :line, :column, description: "syntax error"]
@impl true
def message(exception) do
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
" " <> exception.description
def message(%{file: file, line: line, column: column, description: description}) do
Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <>
" " <> description
end
end
defmodule TokenMissingError do
defexception [:file, :line, description: "expression is incomplete"]
defexception [:file, :line, :column, description: "expression is incomplete"]
@impl true
def message(%{file: file, line: line, description: description}) do
Exception.format_file_line(file && Path.relative_to_cwd(file), line) <> " " <> description
def message(%{file: file, line: line, column: column, description: description}) do
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column) <>
" " <> description
end
end
@@ -950,7 +977,7 @@ defmodule UndefinedFunctionError do
end
defp hint(nil, _function, 0, _loaded?) do
". If you are using the dot syntax, such as map.field or module.function, " <>
". If you are using the dot syntax, such as map.field or module.function(), " <>
"make sure the left side of the dot is an atom or a map"
end
@@ -1329,7 +1356,7 @@ defmodule File.CopyError do
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
location =
case exception.on() do
case exception.on do
"" -> ""
on -> ". #{on}"
end
@@ -1347,7 +1374,7 @@ defmodule File.RenameError do
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
location =
case exception.on() do
case exception.on do
"" -> ""
on -> ". #{on}"
end
+9 -1
View File
@@ -110,6 +110,7 @@ defmodule File do
@type stream_mode ::
encoding_mode()
| :append
| :trim_bom
| {:read_ahead, pos_integer | false}
| {:delayed_write, non_neg_integer, non_neg_integer}
@@ -735,6 +736,7 @@ defmodule File do
File.rename("samples", "tmp")
"""
@doc since: "1.1.0"
@spec rename(Path.t(), Path.t()) :: :ok | {:error, posix}
def rename(source, destination) do
:file.rename(source, destination)
@@ -1503,6 +1505,12 @@ defmodule File do
@doc """
Sets the current working directory.
The current working directory is set for the BEAM globally. This can lead to
race conditions if multiple processes are changing the current working
directory concurrently. To run an external command in a given directory
without changing the global current working directory, use the `:cd` option
of `System.cmd/3` and `Port.open/2`.
Returns `:ok` if successful, `{:error, reason}` otherwise.
"""
@spec cd(Path.t()) :: :ok | {:error, posix}
@@ -1637,7 +1645,7 @@ defmodule File do
See `Stream.run/1` for an example of streaming into a file.
"""
@spec stream!(Path.t(), stream_mode, :line | pos_integer) :: File.Stream.t()
@spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t()
def stream!(path, modes \\ [], line_or_bytes \\ :line) do
modes = normalize_modes(modes, true)
File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes)
+20 -4
View File
@@ -8,8 +8,21 @@ defmodule Function do
iex> add.(1, 2)
3
It is also possible to capture module functions and pass them around
as if they were anonymous functions by using the capture operator `&/1`:
Anonymous functions can also have multiple clauses. All clauses
should expect the same number of arguments:
iex> negate = fn
...> true -> false
...> false -> true
...> end
iex> negate.(false)
true
## The capture operator
It is also possible to capture public module functions and pass them
around as if they were anonymous functions by using the capture
operator `Kernel.SpecialForms.&/1`:
iex> add = &Kernel.+/2
iex> add.(1, 2)
@@ -19,8 +32,9 @@ defmodule Function do
iex> length.("hello")
5
It is also possible to capture a definition in the current module by
skipping the module prefix, such as `&my_fun/2`.
To capture a definition within the current module, you can skip the
module prefix, such as `&my_fun/2`. In those cases, the captured
function can be public (`def`) or private (`defp`).
The capture operator can also be used to create anonymous functions
that expect at least one argument:
@@ -31,6 +45,8 @@ defmodule Function do
In such cases, using the capture operator is no different than using `fn`.
## Internal and external functions
We say that functions that point to definitions residing in modules, such
as `&String.length/1`, are **external** functions. All other functions are
**local** and they are always bound to the file or module that defined them.
+1 -1
View File
@@ -7,7 +7,7 @@ defmodule GenEvent do
If you are interested in implementing an event manager, please read the
"Alternatives" section below. If you have to implement an event handler to
integrate with an existing system, such as Elixir's Logger, please use
`:gen_event` instead.
[`:gen_event`](https://erlang.org/doc/man/gen_event.html) instead.
## Alternatives
+10 -3
View File
@@ -280,6 +280,10 @@ defmodule GenServer do
GenServer.call(__MODULE__, {:add, a, b})
end
def subtract(a, b) do
GenServer.call(__MODULE__, {:subtract, a, b})
end
def handle_call({:add, a, b}, _from, state) do
{:reply, a + b, state}
end
@@ -640,7 +644,7 @@ defmodule GenServer do
This callback is optional.
"""
@callback terminate(reason, state :: term) :: term
when reason: :normal | :shutdown | {:shutdown, term}
when reason: :normal | :shutdown | {:shutdown, term} | term
@doc """
Invoked to change the state of the `GenServer` when a different version of a
@@ -1177,8 +1181,11 @@ defmodule GenServer do
end
@doc """
Returns the `pid` or `{name, node}` of a GenServer process, or `nil` if
no process is associated with the given `server`.
Returns the `pid` or `{name, node}` of a GenServer process, `nil` otherwise.
To be precise, `nil` is returned whenever a `pid` or `{name, node}` cannot
be returned. Note there is no guarantee the returned `pid` or `{name, node}`
is alive, as a process could terminate immediately after it is looked up.
## Examples
+6 -6
View File
@@ -48,8 +48,8 @@ defmodule HashDict do
end
@deprecated message
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))
def update(%HashDict{root: root, size: size}, key, default, fun) when is_function(fun, 1) do
{root, counter} = do_update(root, key, fn -> default end, fun, key_hash(key))
%HashDict{root: root, size: size + counter}
end
@@ -135,25 +135,25 @@ defmodule HashDict do
end
end
defp do_update(node, key, initial, fun, hash) do
defp do_update(node, key, default, fun, hash) do
index = key_mask(hash)
case elem(node, index) do
[] ->
{put_elem(node, index, [key | initial.()]), 1}
{put_elem(node, index, [key | default.()]), 1}
[^key | value] ->
{put_elem(node, index, [key | fun.(value)]), 0}
[k | v] ->
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | initial.()])
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | default.()])
{put_elem(node, index, {k, v, n}), 1}
{^key, value, n} ->
{put_elem(node, index, {key, fun.(value), n}), 0}
{k, v, n} ->
{n, counter} = do_update(n, key, initial, fun, key_shift(hash))
{n, counter} = do_update(n, key, default, fun, key_shift(hash))
{put_elem(node, index, {k, v, n}), counter}
end
end
+11 -9
View File
@@ -31,15 +31,17 @@ defprotocol Inspect do
end
end
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from `Inspect.Algebra` and it
concatenates algebra documents together. In the example above it is
concatenating the string `"MapSet<"`, the document returned by
`Inspect.Algebra.to_doc/2`, and the final string `">"`. All strings are
valid algebra documents that keep their formatting when pretty printed.
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from
`Inspect.Algebra` and it concatenates algebra documents together.
In the example above it is concatenating the string `"#MapSet<"`,
the document returned by `Inspect.Algebra.to_doc/2`, and the final
string `">"`. We prefix the module name `#` to denote the inspect
presentation is not actually valid Elixir syntax.
Since regular strings are valid entities in an algebra document,
an implementation of the `Inspect` protocol may simply return a
string, although that will devoid it of any pretty-printing.
Finally, note strings themselves are valid algebra documents that
keep their formatting when pretty printed. This means your `Inspect`
implementation may simply return a string, although that will devoid
it of any pretty-printing.
## Error handling
@@ -437,7 +439,7 @@ defimpl Inspect, for: Any do
def inspect(%module{} = struct, opts) do
try do
module.__struct__
module.__struct__()
rescue
_ -> Inspect.Map.inspect(struct, opts)
else
+35 -22
View File
@@ -31,22 +31,24 @@ defmodule Inspect.Opts do
options. Useful when implementing the `Inspect` protocol for nested structs
to pass the custom options through.
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents,
defaults to `Inspect.inspect/2`.
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents.
Defaults to `Inspect.inspect/2`.
* `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items. It does not
apply to printable strings nor printable charlists and defaults to 50.
bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option.
If you don't want to limit the number of items to a particular number,
use `:infinity`.
use `:infinity`. It accepts a positive integer or `:infinity`.
Defaults to `50`.
* `:pretty` - if set to `true` enables pretty printing, defaults to `false`.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
* `:printable_limit` - limits the number of characters that are inspected
on printable strings and printable charlists. You can use `String.printable?/1`
and `List.ascii_printable?/1` to check if a given string or charlist is
printable. Defaults to 4096. If you don't want to limit the number of
characters to a particular number, use `:infinity`.
printable. If you don't want to limit the number of characters to a particular
number, use `:infinity`. It accepts a positive integer or `:infinity`.
Defaults to `4096`.
* `:safe` - when `false`, failures while inspecting structs will be raised
as errors instead of being wrapped in the `Inspect.Error` exception. This
@@ -54,7 +56,7 @@ defmodule Inspect.Opts do
implementations.
* `:structs` - when `false`, structs are not formatted by the inspect
protocol, they are instead printed as maps, defaults to `true`.
protocol, they are instead printed as maps. Defaults to `true`.
* `:syntax_colors` - when set to a keyword list of colors the output is
colorized. The keys are types and the values are the colors to use for
@@ -63,10 +65,10 @@ defmodule Inspect.Opts do
`:string`, and `:tuple`. Custom data types may provide their own options.
Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.
* `:width` - defaults to 80 characters, used when pretty is `true` or when
printing to IO devices. Set to 0 to force each item to be printed on its
* `:width` - number of characters per line used when pretty is `true` or when
printing to IO devices. Set to `0` to force each item to be printed on its
own line. If you don't want to limit the number of items to a particular
number, use `:infinity`.
number, use `:infinity`. Defaults to `80`.
"""
@@ -95,13 +97,13 @@ defmodule Inspect.Opts do
charlists: :infer | :as_lists | :as_charlists,
custom_options: keyword,
inspect_fun: (any, t -> Inspect.Algebra.t()),
limit: pos_integer | :infinity,
limit: non_neg_integer | :infinity,
pretty: boolean,
printable_limit: pos_integer | :infinity,
printable_limit: non_neg_integer | :infinity,
safe: boolean,
structs: boolean,
syntax_colors: [{color_key, IO.ANSI.ansidata()}],
width: pos_integer | :infinity
width: non_neg_integer | :infinity
}
end
@@ -148,7 +150,7 @@ defmodule Inspect.Algebra do
iex> Inspect.Algebra.format(doc, 80)
["a", " ", "b"]
Notice the break was represented as is, because we haven't reached
Note that 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")
@@ -266,6 +268,9 @@ defmodule Inspect.Algebra do
when is_binary(doc) or doc in [:doc_nil, :doc_line] or
(is_tuple(doc) and elem(doc, 0) in @docs)
defguardp is_limit(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
# Elixir + Inspect.Opts conveniences
@doc """
@@ -397,13 +402,14 @@ defmodule Inspect.Algebra do
{:lists.reverse(["..." | acc]), simple?}
end
defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) do
defp container_each([term | terms], limit, opts, fun, acc, simple?)
when is_list(terms) and is_limit(limit) do
limit = decrement(limit)
doc = fun.(term, %{opts | limit: limit})
container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc))
end
defp container_each([left | right], limit, opts, fun, acc, simple?) do
defp container_each([left | right], limit, opts, fun, acc, simple?) when is_limit(limit) do
limit = decrement(limit)
left = fun.(left, %{opts | limit: limit})
right = fun.(right, %{opts | limit: limit})
@@ -595,7 +601,7 @@ defmodule Inspect.Algebra do
iex> Inspect.Algebra.format(doc, 80)
["a", "\t", "b"]
Notice the break was represented with the given string, because we didn't
Note that the break was represented with the given string, because we didn't
reach a line limit. Once we do, it is replaced by a newline:
iex> break = Inspect.Algebra.break("\t")
@@ -884,7 +890,7 @@ defmodule Inspect.Algebra do
"""
@spec format(t, non_neg_integer | :infinity) :: iodata
def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
def format(doc, width) when is_doc(doc) and is_width(width) do
format(width, 0, [{0, :flat, doc}])
end
@@ -900,7 +906,12 @@ defmodule Inspect.Algebra do
#
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
@spec fits?(width :: integer(), column :: integer(), break? :: boolean(), entries) :: boolean()
@spec fits?(
width :: non_neg_integer(),
column :: non_neg_integer(),
break? :: boolean(),
entries
) :: boolean()
when entries:
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
@@ -961,7 +972,9 @@ defmodule Inspect.Algebra do
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
@spec format(width :: non_neg_integer() | :infinity, column :: non_neg_integer(), [
{integer, mode, t}
]) :: [binary]
defp format(_, _, []), do: []
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
+22 -5
View File
@@ -317,7 +317,7 @@ defmodule IO do
@spec warn(chardata | String.Chars.t(), Exception.stacktrace()) :: :ok
def warn(message, []) do
message = [to_chardata(message), ?\n]
:elixir_errors.io_warn(nil, nil, message, message)
:elixir_errors.io_warn(0, nil, message, message)
end
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
@@ -327,18 +327,35 @@ defmodule IO do
file = opts[:file]
:elixir_errors.io_warn(
line,
line || 0,
file && List.to_string(file),
message,
[message, ?\n, " ", formatted_trace, ?\n]
)
end
@doc false
def warn_once(key, message, stacktrace_drop_levels) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
if :elixir_config.warn(key, stacktrace) do
warn(message, stacktrace)
else
:ok
end
end
@doc """
Writes a `message` to stderr, along with the current stacktrace.
It returns `:ok` if it succeeds.
Do not call this function at the tail of another function. Due to tail
call optimization, a stacktrace entry would not be added and the
stacktrace would be incorrectly trimmed. Therefore make sure at least
one expression (or an atom such as `:ok`) follows the `IO.warn/1` call.
## Examples
IO.warn("variable bar is unused")
@@ -430,8 +447,8 @@ defmodule IO do
See `IO.getn/3` for a description of return values.
"""
@spec getn(chardata | String.Chars.t(), pos_integer) :: chardata | nodata
@spec getn(device, chardata | String.Chars.t()) :: chardata | nodata
@spec getn(device | chardata | String.Chars.t(), pos_integer | chardata | String.Chars.t()) ::
chardata | nodata
def getn(prompt, count \\ 1)
def getn(prompt, count) when is_integer(count) and count > 0 do
@@ -582,7 +599,7 @@ defmodule IO do
The operation is Unicode unsafe.
Notice that this function treats integers in the given IO data as
Note that this function treats integers in the given IO data as
raw bytes and does not perform any kind of encoding conversion.
If you want to convert from a charlist to a UTF-8-encoded string,
use `chardata_to_string/1` instead. For more information about
+25
View File
@@ -21,6 +21,31 @@ defmodule IO.ANSI do
[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code)
are characters embedded in text used to control formatting, color, and
other output options on video text terminals.
ANSI escapes are typically enabled on all Unix terminals. They are also
available on Windows consoles from Windows 10, although it must be
explicitly enabled for the current user in the registry by running the
following command:
reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1
After running the command above, you must restart your current console.
## Examples
Because the ANSI escape sequences are embedded in text, the normal usage of
these functions is to concatenate their output with text.
formatted_text = IO.ANSI.blue_background() <> "Example" <> IO.ANSI.reset()
IO.puts(formatted_text)
A higher level and more convenient API is also available via `IO.ANSI.format/1`,
where you use atoms to represent each ANSI escape sequence and by default
checks if ANSI is enabled:
IO.puts(IO.ANSI.format([:blue_background, "Example"]))
In case ANSI is disabled, the ANSI escape sequences are simply discarded.
"""
import IO.ANSI.Sequence
+258 -48
View File
@@ -1,6 +1,7 @@
defmodule IO.ANSI.Docs do
@moduledoc false
@bullet_text "• "
@bullets [?*, ?-, ?+]
@spaces [" ", "\n", "\t"]
@@ -46,15 +47,20 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_heading(String.t(), keyword) :: :ok
def print_heading(heading, options \\ []) do
IO.puts(IO.ANSI.reset())
@spec print_headings([String.t()], keyword) :: :ok
def print_headings(headings, options \\ []) do
options = Keyword.merge(default_options(), options)
newline_after_block(options)
width = options[:width]
padding = div(width + String.length(heading), 2)
heading = heading |> String.pad_leading(padding) |> String.pad_trailing(width)
write(:doc_title, heading, options)
newline_after_block()
for heading <- headings do
padding = div(width + String.length(heading), 2)
heading = String.pad_leading(heading, padding)
heading = if options[:enabled], do: String.pad_trailing(heading, width), else: heading
write(:doc_title, heading, options)
end
newline_after_block(options)
end
@doc """
@@ -78,11 +84,11 @@ defmodule IO.ANSI.Docs do
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
IO.puts([metadata_label(key, options), ' ', to_string(value)])
IO.puts([metadata_label(key, options), ?\s, to_string(value)])
{:delegate_to, {m, f, a}}, _printed ->
label = metadata_label(:delegate_to, options)
IO.puts([label, ' ', Exception.format_mfa(m, f, a)])
IO.puts([label, ?\s, Exception.format_mfa(m, f, a)])
_metadata, printed ->
printed
@@ -90,21 +96,204 @@ defmodule IO.ANSI.Docs do
end
defp metadata_label(key, options) do
if options[:enabled] do
"#{color(:doc_metadata, options)}#{key}:#{IO.ANSI.reset()}"
else
"#{key}:"
end
"#{color(:doc_metadata, options)}#{key}:#{maybe_reset(options)}"
end
@doc """
Prints the documentation body.
Prints the documentation body `doc` according to `format`.
In addition to the printing string, takes a set of `options`
defined in `default_options/0`.
It takes a set of `options` defined in `default_options/0`.
"""
@spec print(String.t(), keyword) :: :ok
def print(doc, options \\ []) do
@spec print(term(), String.t(), keyword) :: :ok
def print(doc, format, options \\ [])
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
print_markdown(doc, options)
end
def print(doc, "application/erlang+html", options) when is_list(options) do
print_erlang_html(doc, options)
end
def print(_doc, format, options) when is_binary(format) and is_list(options) do
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
end
## Erlang+html
def print_erlang_html(doc, options) do
options = Keyword.merge(default_options(), options)
IO.write(traverse_erlang_html(doc, "", options))
end
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
text
end
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
for node <- nodes do
traverse_erlang_html(node, indent, options)
end
end
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
prefix = indent <> quote_prefix(options)
content =
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.trim_trailing()
[
prefix,
class |> to_string() |> String.upcase(),
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
]
|> newline_cons()
end
defp traverse_erlang_html({:p, _, entries}, indent, options) do
[indent | handle_erlang_html_text(entries, indent, options)]
end
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
end
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
end
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
end
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
end
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
end
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
end
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
[]
end
defp traverse_erlang_html({:i, _, entries}, indent, options) do
inline_text("_", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:em, _, entries}, indent, options) do
inline_text("*", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:code, _, entries}, indent, options) do
inline_text("`", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
string =
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
end
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
if href = attributes[:href] do
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
else
traverse_erlang_html(entries, indent, options)
end
end
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
traverse_erlang_html(entries, indent, options)
end
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
["#{indent} ", @bullet_text | handle_erlang_html_text(entries, indent <> " ", options)]
end
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
end
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
if attributes[:class] == "types" do
types =
for {:li, _, lines} <- entries,
line <- lines,
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
if types != [] do
["#{indent}Typespecs:\n\n", types, ?\n]
else
[]
end
else
for {:li, _, lines} <- entries do
["#{indent} ", @bullet_text | handle_erlang_html_text(lines, indent <> " ", options)]
end
end
end
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
[
"#{indent} ",
Integer.to_string(i),
". " | handle_erlang_html_text(lines, indent <> " ", options)
]
end
end
defp traverse_erlang_html({tag, _, entries}, indent, options) do
[
indent <> "<#{tag}>\n",
traverse_erlang_html(entries, indent <> " ", options)
|> IO.iodata_to_binary()
|> String.trim_trailing(),
"\n" <> indent <> "</#{tag}>"
]
|> newline_cons()
end
defp newline_cons(text) do
[text | "\n\n"]
end
defp handle_erlang_html_text(entries, indent, options) do
if Enum.all?(entries, &inline_html?/1) do
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.split(@spaces)
|> wrap_text(options[:width], indent, true, "", [])
|> tl()
|> newline_cons()
else
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.trim_leading()
end
end
defp inline_html?(binary) when is_binary(binary), do: true
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :br], do: true
defp inline_html?(_), do: false
## Markdown
def print_markdown(doc, options) do
options = Keyword.merge(default_options(), options)
doc
@@ -181,16 +370,16 @@ defmodule IO.ANSI.Docs do
end
end
## Headings
### Headings
defp write_heading(heading, rest, text, indent, options) do
write_text(text, indent, options)
write(:doc_headings, heading, options)
newline_after_block()
newline_after_block(options)
process(rest, [], "", options)
end
## Quotes
### Quotes
defp process_quote([], lines, indent, options) do
write_quote(lines, indent, options, false)
@@ -223,21 +412,19 @@ defmodule IO.ANSI.Docs do
)
end
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{IO.ANSI.reset()}"
defp write_empty_quote_line(options) do
options
|> quote_prefix()
|> IO.puts()
end
## Lists
### Lists
defp process_rest(stripped, rest, count, text, indent, options) do
case stripped do
<<bullet, ?\s, item::binary>> when bullet in @bullets ->
write_text(text, indent, options)
process_list("• ", item, rest, count, indent, options)
process_list(@bullet_text, item, rest, count, indent, options)
<<d1, ?., ?\s, item::binary>> when d1 in ?0..?9 ->
write_text(text, indent, options)
@@ -257,10 +444,12 @@ defmodule IO.ANSI.Docs do
entry = if indent == "", do: " " <> entry, else: entry
new_indent = indent <> String.duplicate(" ", String.length(entry))
{contents, rest, done} = process_list_next(rest, count, byte_size(new_indent), [])
{contents, rest, done} =
process_list_next(rest, count, byte_size(new_indent) - byte_size(indent), [])
process(contents, [indent <> entry <> line, :no_wrap], new_indent, options)
if done, do: newline_after_block()
if done, do: newline_after_block(options)
process(rest, [], indent, options)
end
@@ -301,7 +490,7 @@ defmodule IO.ANSI.Docs do
end
end
## Text
### Text
defp write_text(text, indent, options) do
case Enum.reverse(text) do
@@ -325,7 +514,7 @@ defmodule IO.ANSI.Docs do
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
unless no_wrap, do: newline_after_block()
unless no_wrap, do: newline_after_block(options)
end
defp format_text(text, options) do
@@ -334,7 +523,7 @@ defmodule IO.ANSI.Docs do
|> handle_inline(options)
end
## Code blocks
### Code blocks
defp process_code([], code, indent, options) do
write_code(code, indent, options)
@@ -373,15 +562,15 @@ defmodule IO.ANSI.Docs do
defp write_code(code, indent, options) do
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
newline_after_block()
newline_after_block(options)
end
## Tables
### Tables
defp process_table(lines, indent, options) do
{table, rest} = Enum.split_while(lines, &table_line?/1)
table_lines(table, options)
newline_after_block()
newline_after_block(options)
process(rest, [], indent, options)
end
@@ -407,7 +596,7 @@ defmodule IO.ANSI.Docs do
line
|> String.trim(" ")
|> String.trim("|")
|> String.split("|")
|> String.split(~r{(?<!\\)\|})
|> Enum.map(&render_column(&1, options))
end
@@ -524,7 +713,7 @@ defmodule IO.ANSI.Docs do
defp strip_spaces(rest, acc, _max), do: {rest, acc}
defp write(style, string, options) do
IO.puts([color(style, options), string, IO.ANSI.reset()])
IO.puts([color(style, options), string, maybe_reset(options)])
end
defp write_with_wrap([], _available, _indent, _first, _prefix) do
@@ -534,7 +723,7 @@ defmodule IO.ANSI.Docs do
defp write_with_wrap(words, available, indent, first, prefix) do
words
|> wrap_text(available, indent, first, prefix, [])
|> Enum.join("\n")
|> tl()
|> IO.puts()
end
@@ -547,7 +736,7 @@ defmodule IO.ANSI.Docs do
{words, rest} = take_words(words, available - prefix_length, [])
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
wrap_text(rest, available, indent, false, prefix, [line | wrapped_lines])
wrap_text(rest, available, indent, false, prefix, [line, ?\n | wrapped_lines])
end
defp take_words([word | words], available, acc) do
@@ -621,7 +810,7 @@ defmodule IO.ANSI.Docs do
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
# Inline start
### Inline start
defp handle_inline(<<?*, ?*, rest::binary>>, options) do
handle_inline(rest, ?d, ["**"], [], options)
@@ -635,7 +824,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, nil, [], [], options)
end
# Inline delimiters
### Inline delimiters
defp handle_inline(<<delimiter, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
when rest != "" and delimiter in @delimiters do
@@ -652,7 +841,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, ?`, ["`"], [Enum.reverse(buffer) | acc], options)
end
# Clauses for handling escape
### Clauses for handling escape
defp handle_inline(<<?\\, ?\\, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
when rest != "" do
@@ -673,7 +862,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, limit, [mark | buffer], acc, options)
end
# Inline end
### Inline end
defp handle_inline(<<?*, ?*, delimiter, rest::binary>>, ?d, buffer, acc, options)
when delimiter in @delimiters do
@@ -701,7 +890,7 @@ defmodule IO.ANSI.Docs do
handle_inline(rest, nil, [], [inline_buffer(buffer, options) | acc], options)
end
# Catch all
### Catch all
defp handle_inline(<<char, rest::binary>>, mark, buffer, acc, options) do
handle_inline(rest, mark, [char | buffer], acc, options)
@@ -712,8 +901,24 @@ defmodule IO.ANSI.Docs do
end
defp inline_buffer(buffer, options) do
[h | t] = Enum.reverse([IO.ANSI.reset() | buffer])
[color_for(h, options) | t]
[mark | t] = Enum.reverse(buffer)
inline_text(mark, t, options)
end
## Helpers
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
defp heading(text, n, options) do
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
end
defp inline_text(mark, text, options) do
if options[:enabled] do
[[color_for(mark, options) | text] | IO.ANSI.reset()]
else
[[mark | text] | mark]
end
end
defp color_for(mark, colors) do
@@ -726,9 +931,14 @@ defmodule IO.ANSI.Docs do
end
defp color(style, colors) do
color = colors[style]
IO.ANSI.format_fragment(color, colors[:enabled])
IO.ANSI.format_fragment(colors[style], colors[:enabled])
end
defp newline_after_block, do: IO.puts(IO.ANSI.reset())
defp newline_after_block(options) do
IO.puts(maybe_reset(options))
end
defp maybe_reset(options) do
if options[:enabled], do: IO.ANSI.reset(), else: ""
end
end
+299 -99
View File
@@ -93,7 +93,7 @@ defmodule Kernel do
### Protocols
Protocols add polymorphic dispatch to Elixir. They are contracts
implementable by data types. See `defprotocol/2` for more information on
implementable by data types. See `Protocol` for more information on
protocols. Elixir provides the following protocols in the standard library:
* `Collectable` - collects data into a data type
@@ -127,19 +127,19 @@ defmodule Kernel do
Elixir documentation also includes supporting documents under the
"Pages" section. Those are:
* [Compatibility and Deprecations](compatibility-and-deprecations.html) - lists
* [Compatibility and Deprecations](compatibility-and-deprecations.md) - lists
compatibility between every Elixir version and Erlang/OTP, release schema;
lists all deprecated functions, when they were deprecated and alternatives
* [Library Guidelines](library-guidelines.html) - general guidelines, anti-patterns,
* [Library Guidelines](library-guidelines.md) - general guidelines, anti-patterns,
and rules for those writing libraries
* [Naming Conventions](naming-conventions.html) - naming conventions for Elixir code
* [Operators](operators.html) - lists all Elixir operators and their precedence
* [Patterns and Guards](patterns-and-guards.html) - an introduction to patterns,
* [Naming Conventions](naming-conventions.md) - naming conventions for Elixir code
* [Operators](operators.md) - lists all Elixir operators and their precedences
* [Patterns and Guards](patterns-and-guards.md) - an introduction to patterns,
guards, and extensions
* [Syntax Reference](syntax-reference.html) - the language syntax reference
* [Typespecs](typespecs.html)- types and function specifications, including list of types
* [Unicode Syntax](unicode-syntax.html) - outlines Elixir support for Unicode
* [Writing Documentation](writing-documentation.html) - guidelines for writing
* [Syntax Reference](syntax-reference.md) - the language syntax reference
* [Typespecs](typespecs.md)- types and function specifications, including list of types
* [Unicode Syntax](unicode-syntax.md) - outlines Elixir support for Unicode
* [Writing Documentation](writing-documentation.md) - guidelines for writing
documentation in Elixir
## Guards
@@ -156,7 +156,7 @@ defmodule Kernel do
or equal to 16. Guards also support joining multiple conditions with
`and` and `or`. The whole guard is true if all guard expressions will
evaluate to `true`. A more complete introduction to guards is available
[in the "Patterns and Guards" page](patterns-and-guards.html).
[in the "Patterns and Guards" page](patterns-and-guards.md).
## Inlining
@@ -1193,7 +1193,7 @@ defmodule Kernel do
end
@doc """
Arithmetic addition.
Arithmetic addition operator.
Allowed in guard tests. Inlined by the compiler.
@@ -1213,7 +1213,7 @@ defmodule Kernel do
end
@doc """
Arithmetic subtraction.
Arithmetic subtraction operator.
Allowed in guard tests. Inlined by the compiler.
@@ -1233,7 +1233,7 @@ defmodule Kernel do
end
@doc """
Arithmetic unary plus.
Arithmetic positive unary operator.
Allowed in guard tests. Inlined by the compiler.
@@ -1251,7 +1251,7 @@ defmodule Kernel do
end
@doc """
Arithmetic unary minus.
Arithmetic negative unary operator.
Allowed in guard tests. Inlined by the compiler.
@@ -1271,7 +1271,7 @@ defmodule Kernel do
end
@doc """
Arithmetic multiplication.
Arithmetic multiplication operator.
Allowed in guard tests. Inlined by the compiler.
@@ -1291,7 +1291,7 @@ defmodule Kernel do
end
@doc """
Arithmetic division.
Arithmetic division operator.
The result is always a float. Use `div/2` and `rem/2` if you want
an integer division or the remainder.
@@ -1322,7 +1322,7 @@ defmodule Kernel do
end
@doc """
Concatenates a proper list and a term, returning a list.
List concatenation operator. Concatenates a proper list and a term, returning a list.
The complexity of `a ++ b` is proportional to `length(a)`, so avoid repeatedly
appending to lists of arbitrary length, for example, `list ++ [element]`.
@@ -1360,13 +1360,19 @@ defmodule Kernel do
end
@doc """
Removes the first occurrence of an element on the left list
List subtraction operator. Removes the first occurrence of an element on the left list
for each element on the right.
The complexity of `a -- b` is proportional to `length(a) * length(b)`,
meaning that it will be very slow if both `a` and `b` are long lists.
In such cases, consider converting each list to a `MapSet` and using
`MapSet.difference/2`.
Before Erlang/OTP 22, the complexity of `a -- b` was proportional to
`length(a) * length(b)`, meaning that it would be very slow if
both `a` and `b` were long lists. In such cases, consider
converting each list to a `MapSet` and using `MapSet.difference/2`.
As of Erlang/OTP 22, this operation is significantly faster even if both
lists are very long, and using `--/2` is usually faster and uses less
memory than using the `MapSet`-based alternative mentioned above.
See also the [Erlang efficiency
guide](https://erlang.org/doc/efficiency_guide/retired_myths.html).
Inlined by the compiler.
@@ -1378,6 +1384,16 @@ defmodule Kernel do
iex> [1, 2, 3, 2, 1] -- [1, 2, 2]
[3, 1]
The `--/2` operator is right associative, meaning:
iex> [1, 2, 3] -- [2] -- [3]
[1, 3]
As it is equivalent to:
iex> [1, 2, 3] -- ([2] -- [3])
[1, 3]
"""
@spec list -- list :: list
def left -- right do
@@ -1385,9 +1401,9 @@ defmodule Kernel do
end
@doc """
Boolean not.
Strictly boolean "not" operator.
`arg` must be a boolean; if it's not, an `ArgumentError` exception is raised.
`value` must be a boolean; if it's not, an `ArgumentError` exception is raised.
Allowed in guard tests. Inlined by the compiler.
@@ -1405,7 +1421,9 @@ defmodule Kernel do
end
@doc """
Returns `true` if left is less than right.
Less-than operator.
Returns `true` if `left` is less than `right`.
All terms in Elixir can be compared with each other.
@@ -1424,7 +1442,9 @@ defmodule Kernel do
end
@doc """
Returns `true` if left is more than right.
Greater-than operator.
Returns `true` if `left` is more than `right`.
All terms in Elixir can be compared with each other.
@@ -1443,7 +1463,9 @@ defmodule Kernel do
end
@doc """
Returns `true` if left is less than or equal to right.
Less-than or equal to operator.
Returns `true` if `left` is less than or equal to `right`.
All terms in Elixir can be compared with each other.
@@ -1462,7 +1484,9 @@ defmodule Kernel do
end
@doc """
Returns `true` if left is more than or equal to right.
Greater-than or equal to operator.
Returns `true` if `left` is more than or equal to `right`.
All terms in Elixir can be compared with each other.
@@ -1481,7 +1505,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if the two terms are equal.
Equal to operator. Returns `true` if the two terms are equal.
This operator considers 1 and 1.0 to be equal. For stricter
semantics, use `===/2` instead.
@@ -1506,6 +1530,8 @@ defmodule Kernel do
end
@doc """
Not equal to operator.
Returns `true` if the two terms are not equal.
This operator considers 1 and 1.0 to be equal. For match
@@ -1531,6 +1557,8 @@ defmodule Kernel do
end
@doc """
Strictly equal to operator.
Returns `true` if the two terms are exactly equal.
The terms are only considered to be exactly equal if they
@@ -1558,7 +1586,10 @@ defmodule Kernel do
end
@doc """
Strictly not equal to operator.
Returns `true` if the two terms are not exactly equal.
See `===/2` for a definition of what is considered "exactly equal".
All terms in Elixir can be compared with each other.
@@ -1629,7 +1660,7 @@ defmodule Kernel do
end
@doc """
Boolean or.
Strictly boolean "or" operator.
If `left` is `true`, returns `true`; otherwise returns `right`.
@@ -1643,9 +1674,13 @@ defmodule Kernel do
iex> true or false
true
iex> false or 42
42
iex> 42 or false
** (BadBooleanError) expected a boolean on left-side of "or", got: 42
"""
@doc guard: true
defmacro left or right do
@@ -1657,7 +1692,7 @@ defmodule Kernel do
end
@doc """
Boolean and.
Strictly boolean "and" operator.
If `left` is `false`, returns `false`; otherwise returns `right`.
@@ -1670,9 +1705,13 @@ defmodule Kernel do
iex> true and false
false
iex> true and "yay!"
"yay!"
iex> "yay!" and true
** (BadBooleanError) expected a boolean on left-side of "and", got: "yay!"
"""
@doc guard: true
defmacro left and right do
@@ -1696,9 +1735,9 @@ defmodule Kernel do
end
@doc """
Boolean not.
Boolean "not" operator.
Receives any argument (not just booleans) and returns `true` if the argument
Receives any value (not just booleans) and returns `true` if `value`
is `false` or `nil`; returns `false` otherwise.
Not allowed in guard clauses.
@@ -1741,7 +1780,7 @@ defmodule Kernel do
end
@doc """
Concatenates two binaries.
Binary concatenation operator. Concatenates two binaries.
## Examples
@@ -1911,7 +1950,7 @@ defmodule Kernel do
Works like `raise/1` but does not generate a new stacktrace.
Notice that `__STACKTRACE__` can be used inside catch/rescue
Note that `__STACKTRACE__` can be used inside catch/rescue
to retrieve the current stacktrace.
## Examples
@@ -1983,11 +2022,12 @@ defmodule Kernel do
end
@doc """
Matches the term on the `left` against the regular expression or string on the
`right`.
Text-based match operator. Matches the term on the `left`
against the regular expression or string on the `right`.
Returns `true` if `left` matches `right` (if it's a regular expression)
or contains `right` (if it's a string).
If `right` is a regular expression, returns `true` if `left` matches right.
If `right` is a string, returns `true` if `left` contains `right`.
## Examples
@@ -1997,12 +2037,18 @@ defmodule Kernel do
iex> "abcd" =~ ~r/e/
false
iex> "abcd" =~ ~r//
true
iex> "abcd" =~ "bc"
true
iex> "abcd" =~ "ad"
false
iex> "abcd" =~ "abcd"
true
iex> "abcd" =~ ""
true
@@ -2250,6 +2296,134 @@ defmodule Kernel do
end
end
@doc """
Returns true if `term` is a struct of `name`; otherwise returns `false`.
Allowed in guard tests.
## Examples
iex> is_struct(URI.parse("/"), URI)
true
iex> is_struct(URI.parse("/"), Macro.Env)
false
"""
@doc since: "1.11.0", guard: true
defmacro is_struct(term, name) do
case __CALLER__.context do
nil ->
quote do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name} -> true
_ -> false
end
_ ->
raise ArgumentError
end
end
:match ->
invalid_match!(:is_struct)
:guard ->
quote do
is_map(unquote(term)) and
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name)
end
end
end
@doc """
Returns true if `term` is an exception; otherwise returns `false`.
Allowed in guard tests.
## Examples
iex> is_exception(%RuntimeError{})
true
iex> is_exception(%{})
false
"""
@doc since: "1.11.0", guard: true
defmacro is_exception(term) do
case __CALLER__.context do
nil ->
quote do
case unquote(term) do
%_{__exception__: true} -> true
_ -> false
end
end
:match ->
invalid_match!(:is_exception)
:guard ->
quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end
end
end
@doc """
Returns true if `term` is an exception of `name`; otherwise returns `false`.
Allowed in guard tests.
## Examples
iex> is_exception(%RuntimeError{}, RuntimeError)
true
iex> is_exception(%RuntimeError{}, Macro.Env)
false
"""
@doc since: "1.11.0", guard: true
defmacro is_exception(term, name) do
case __CALLER__.context do
nil ->
quote do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: true} -> true
_ -> false
end
_ ->
raise ArgumentError
end
end
:match ->
invalid_match!(:is_exception)
:guard ->
quote do
is_map(unquote(term)) and
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end
end
end
@doc """
Gets a value from a nested structure.
@@ -2263,8 +2437,7 @@ defmodule Kernel do
iex> get_in(users, ["john", :age])
27
In case any of the entries in the middle returns `nil`, `nil` will
be returned as per the `Access` module:
In case any of the keys returns `nil`, `nil` will be returned:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users, ["unknown", :age])
@@ -2280,9 +2453,6 @@ defmodule Kernel do
* a function to be invoked next
This means `get_in/2` can be extended to provide custom lookups.
The downside is that functions cannot be stored as keys in the accessed
data structures.
In the example below, we use a function to get all the maps inside
a list:
@@ -2341,6 +2511,12 @@ defmodule Kernel do
function. If the key is a function, it will be invoked
as specified in `get_and_update_in/3`.
`data` is a nested structure (that is, a map, keyword
list, or struct that implements the `Access` behaviour).
The `fun` argument receives the value of `key` (or `nil`
if `key` is not present) and the result replaces the value
in the structure.
## Examples
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
@@ -2886,7 +3062,7 @@ defmodule Kernel do
end
@doc """
Reads and writes attributes of the current module.
Module attribute unary operator. Reads and writes attributes in the current module.
The canonical example for attributes is annotating that a module
implements an OTP behaviour, such as `GenServer`:
@@ -2910,7 +3086,7 @@ defmodule Kernel do
will be available at compile-time. Custom attributes may be configured to
behave closer to Erlang by using `Module.register_attribute/3`.
Finally, notice that attributes can also be read inside functions:
Finally, note that attributes can also be read inside functions:
defmodule MyServer do
@my_data 11
@@ -3247,7 +3423,7 @@ defmodule Kernel do
end
@doc """
Returns a range with the specified `first` and `last` integers.
Range creation operator. Returns a range with the specified `first` and `last` integers.
If last is larger than first, the range will be increasing from
first to last. If first is larger than last, the range will be
@@ -3302,6 +3478,8 @@ defmodule Kernel do
end
@doc """
Boolean "and" operator.
Provides a short-circuit operator that evaluates and returns
the second expression only if the first one evaluates to a truthy value
(neither `false` nor `nil`). Returns the first expression
@@ -3341,6 +3519,8 @@ defmodule Kernel do
end
@doc """
Boolean "or" operator.
Provides a short-circuit operator that evaluates and returns the second
expression only if the first one does not evaluate to a truthy value (that is,
it is either `nil` or `false`). Returns the first expression otherwise.
@@ -3520,7 +3700,7 @@ defmodule Kernel do
end
@doc """
Checks if the element on the left-hand side is a member of the
Membership operator. Checks if the element on the left-hand side is a member of the
collection on the right-hand side.
## Examples
@@ -3576,7 +3756,7 @@ defmodule Kernel do
"""
@doc guard: true
defmacro left in right do
in_module? = __CALLER__.context == nil
in_body? = __CALLER__.context == nil
expand =
case bootstrapped?(Macro) do
@@ -3585,7 +3765,7 @@ defmodule Kernel do
end
case expand.(right) do
[] when not in_module? ->
[] when not in_body? ->
false
[] ->
@@ -3594,28 +3774,28 @@ defmodule Kernel do
false
end
[head | tail] = list when not in_module? ->
in_var(in_module?, left, &in_list(&1, head, tail, expand, list, in_module?))
[head | tail] = list when not in_body? ->
in_list(left, head, tail, expand, list, in_body?)
[_ | _] = list when in_module? ->
[_ | _] = list when in_body? ->
case ensure_evaled(list, {0, []}, expand) do
{[head | tail], {_, []}} ->
in_var(in_module?, left, &in_list(&1, head, tail, expand, list, in_module?))
in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
{[head | tail], {_, vars_values}} ->
{vars, values} = :lists.unzip(:lists.reverse(vars_values))
is_in_list = &in_list(&1, head, tail, expand, list, in_module?)
is_in_list = &in_list(&1, head, tail, expand, list, in_body?)
quote do
{unquote_splicing(vars)} = {unquote_splicing(values)}
unquote(in_var(in_module?, left, is_in_list))
unquote(in_var(in_body?, left, is_in_list))
end
end
{:%{}, _meta, [__struct__: Elixir.Range, first: first, last: last]} ->
in_var(in_module?, left, &in_range(&1, expand.(first), expand.(last)))
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last)))
right when in_module? ->
right when in_body? ->
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
%{__struct__: Elixir.Range, first: _, last: _} ->
@@ -3724,18 +3904,12 @@ defmodule Kernel do
end
end
defp in_list(left, head, tail, expand, right, in_module?) do
[head | tail] =
:lists.foldl(
&[comp(left, &1, expand, right, in_module?) | &2],
[],
[head | tail]
)
:lists.foldl(&quote(do: :erlang.orelse(unquote(&1), unquote(&2))), head, tail)
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
:lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
end
defp comp(left, {:|, _, [head, tail]}, expand, right, in_module?) do
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
case expand.(tail) do
[] ->
quote(do: :erlang."=:="(unquote(left), unquote(head)))
@@ -3744,11 +3918,11 @@ defmodule Kernel do
quote do
:erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right, in_module?))
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
)
end
tail when in_module? ->
tail when in_body? ->
quote do
:erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)),
@@ -3761,7 +3935,7 @@ defmodule Kernel do
end
end
defp comp(left, right, _expand, _right, _in_module?) do
defp comp(left, right, _expand, _right, _in_body?) do
quote(do: :erlang."=:="(unquote(left), unquote(right)))
end
@@ -3784,13 +3958,20 @@ defmodule Kernel do
end
@doc """
When used inside quoting, marks that the given variable should
not be hygienized.
Marks that the given variable should not be hygienized.
The argument can be either a variable unquoted or in standard tuple form
`{name, meta, context}`.
This macro expects a variable and it is typically invoked
inside `Kernel.SpecialForms.quote/2` to mark that a variable
should not be hygienized. See `Kernel.SpecialForms.quote/2`
for more information.
## Examples
iex> Kernel.var!(example) = 1
1
iex> Kernel.var!(example)
1
Check `Kernel.SpecialForms.quote/2` for more information.
"""
defmacro var!(var, context \\ nil)
@@ -4091,24 +4272,24 @@ defmodule Kernel do
## `rescue`/`catch`/`after`/`else`
Function bodies support `rescue`, `catch`, `after`, and `else` as `Kernel.SpecialForms.try/1`
does. For example, the following two functions are equivalent:
does (known as "implicit try"). For example, the following two functions are equivalent:
def format(value) do
def convert(number) do
try do
format!(value)
catch
:exit, reason -> {:error, reason}
String.to_integer(number)
rescue
e in ArgumentError -> {:error, e.message}
end
end
def format(value) do
format!(value)
catch
:exit, reason -> {:error, reason}
def convert(number) do
String.to_integer(number)
rescue
e in ArgumentError -> {:error, e.message}
end
"""
defmacro def(call, expr \\ []) do
defmacro def(call, expr \\ nil) do
define(:def, call, expr, __CALLER__)
end
@@ -4138,7 +4319,7 @@ defmodule Kernel do
** (UndefinedFunctionError) undefined function Foo.sum/2
"""
defmacro defp(call, expr \\ []) do
defmacro defp(call, expr \\ nil) do
define(:defp, call, expr, __CALLER__)
end
@@ -4166,7 +4347,7 @@ defmodule Kernel do
end
"""
defmacro defmacro(call, expr \\ []) do
defmacro defmacro(call, expr \\ nil) do
define(:defmacro, call, expr, __CALLER__)
end
@@ -4182,7 +4363,7 @@ defmodule Kernel do
naming and default arguments.
"""
defmacro defmacrop(call, expr \\ []) do
defmacro defmacrop(call, expr \\ nil) do
define(:defmacrop, call, expr, __CALLER__)
end
@@ -4677,7 +4858,7 @@ defmodule Kernel do
macro_definition =
case impls do
[] ->
define(kind, call, [], env)
define(kind, call, nil, env)
[guard] ->
quoted =
@@ -4727,8 +4908,8 @@ defmodule Kernel do
## Examples
For example, in order to write test cases using the `ExUnit` framework
provided with Elixir, a developer should `use` the `ExUnit.Case` module:
For example, to write test cases using the `ExUnit` framework provided
with Elixir, a developer should `use` the `ExUnit.Case` module:
defmodule AssertionTest do
use ExUnit.Case, async: true
@@ -4738,8 +4919,11 @@ defmodule Kernel do
end
end
In this example, `ExUnit.Case.__using__/1` is called with the keyword list
`[async: true]` as its argument; `use/2` translates to:
In this example, Elixir will call the `__using__/1` macro in the
`ExUnit.Case` module with the keyword list `[async: true]` as its
argument.
In other words, `use/2` translates to:
defmodule AssertionTest do
require ExUnit.Case
@@ -4750,7 +4934,7 @@ defmodule Kernel do
end
end
`ExUnit.Case` will then define the `__using__/1` macro:
where `ExUnit.Case` defines the `__using__/1` macro:
defmodule ExUnit.Case do
defmacro __using__(opts) do
@@ -4885,6 +5069,18 @@ defmodule Kernel do
defmacro defdelegate(funs, opts) do
funs = Macro.escape(funs, unquote: true)
# don't add compile-time dependency on :to
opts =
with true <- is_list(opts),
{:ok, target} <- Keyword.fetch(opts, :to),
{:__aliases__, _, _} <- target do
target = Macro.expand(target, %{__CALLER__ | function: {:__info__, 1}})
Keyword.replace!(opts, :to, target)
else
_ ->
opts
end
quote bind_quoted: [funs: funs, opts: opts] do
target =
Keyword.get(opts, :to) || raise ArgumentError, "expected to: to be given as argument"
@@ -5388,7 +5584,8 @@ defmodule Kernel do
true ->
parts = String.split(string)
parts_with_trailing_comma = :lists.filter(&(:binary.last(&1) == ?,), parts)
parts_with_trailing_comma =
:lists.filter(&(byte_size(&1) > 1 and :binary.last(&1) == ?,), parts)
if parts_with_trailing_comma != [] do
stacktrace = Macro.Env.stacktrace(caller)
@@ -5483,9 +5680,12 @@ defmodule Kernel do
end
@doc false
# TODO: Remove on v2.0 (also hard-coded in elixir_dispatch)
@deprecated "Use Kernel.to_charlist/1 instead"
defmacro to_char_list(arg) do
IO.warn(
"Kernel.to_char_list/1 is deprecated, use Kernel.to_charlist/1 instead",
Macro.Env.stacktrace(__CALLER__)
)
quote(do: Kernel.to_charlist(unquote(arg)))
end
end
+6 -2
View File
@@ -498,9 +498,12 @@ defmodule Kernel.CLI do
verbose_opts =
if config.verbose_compile do
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 15s)")]
[each_file: &IO.puts("Compiling #{Path.relative_to_cwd(&1)}")]
else
[]
[
each_long_compilation:
&IO.puts("Compiling #{Path.relative_to_cwd(&1)} (it's taking more than 10s)")
]
end
profile_opts =
@@ -525,6 +528,7 @@ defmodule Kernel.CLI do
defp filter_patterns(pattern) do
pattern
|> Path.expand()
|> Path.wildcard()
|> :lists.usort()
|> Enum.filter(&File.regular?/1)
+19 -21
View File
@@ -29,6 +29,11 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :stop)
end
@doc false
def add_require(pid, module) when is_atom(module) do
:gen_server.cast(pid, {:add_require, module})
end
@doc false
def add_import(pid, module, fas, line, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_import, module, fas, line, warn})
@@ -45,13 +50,8 @@ defmodule Kernel.LexicalTracker do
end
@doc false
def remote_struct(pid, module) when is_atom(module) do
:gen_server.cast(pid, {:remote_struct, module})
end
@doc false
def import_dispatch(pid, module, fa) when is_atom(module) do
:gen_server.cast(pid, {:import_dispatch, module, fa})
def import_dispatch(pid, module, fa, mode) when is_atom(module) do
:gen_server.cast(pid, {:import_dispatch, module, fa, mode})
end
@doc false
@@ -106,7 +106,7 @@ defmodule Kernel.LexicalTracker do
state = %{
directives: %{},
references: %{},
structs: %{},
exports: %{},
cache: %{},
compile_env: :ordsets.new(),
file: nil
@@ -127,7 +127,7 @@ defmodule Kernel.LexicalTracker do
def handle_call(:references, _from, state) do
{compile, runtime} = partition(Map.to_list(state.references), [], [])
{:reply, {compile, Map.keys(state.structs), runtime, state.compile_env}, state}
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
end
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
@@ -142,18 +142,13 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | cache: Map.put(cache, key, value)}}
end
def handle_cast({:remote_struct, module}, state) do
structs = Map.put(state.structs, module, true)
{:noreply, %{state | structs: structs}}
end
def handle_cast({:remote_dispatch, module, mode}, state) do
references = add_reference(state.references, module, mode)
{:noreply, %{state | references: references}}
end
def handle_cast({:import_dispatch, module, {function, arity}}, state) do
state = add_import_dispatch(state, module, function, arity)
def handle_cast({:import_dispatch, module, {function, arity}, mode}, state) do
state = add_import_dispatch(state, module, function, arity, mode)
{:noreply, state}
end
@@ -173,6 +168,10 @@ defmodule Kernel.LexicalTracker do
{:noreply, update_in(state.compile_env, &:ordsets.add_element({app, path, return}, &1))}
end
def handle_cast({:add_require, module}, state) do
{:noreply, put_in(state.exports[module], true)}
end
def handle_cast({:add_import, module, fas, line, warn}, state) do
directives =
state.directives
@@ -227,14 +226,13 @@ defmodule Kernel.LexicalTracker do
end
end
defp add_import_dispatch(state, module, function, arity) do
defp add_import_dispatch(state, module, function, arity, mode) do
directives =
add_dispatch(state.directives, module, :import)
state.directives
|> add_dispatch(module, :import)
|> add_dispatch({module, function, arity}, :import)
# Always compile time because we depend
# on the module at compile time
references = add_reference(state.references, module, :compile)
references = add_reference(state.references, module, mode)
%{state | directives: directives, references: references}
end
+230 -161
View File
@@ -15,7 +15,7 @@ defmodule Kernel.ParallelCompiler do
always awaited on by calling `Task.await/1`
"""
@doc since: "1.6.0"
def async(fun) when is_function(fun) do
def async(fun) when is_function(fun, 0) do
if parent = :erlang.get(:elixir_compiler_pid) do
file = :erlang.get(:elixir_compiler_file)
dest = :erlang.get(:elixir_compiler_dest)
@@ -63,12 +63,16 @@ defmodule Kernel.ParallelCompiler do
* `:each_cycle` - after the given files are compiled, invokes this function
that should return the following values:
* `{:compile, modules}` - to continue compilation with a list of further modules to compile
* `{:runtime, modules}` - to stop compilation and verify the list of modules because
dependent modules have changed
* `{:compile, modules, warnings}` - to continue compilation with a list of
further modules to compile
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
of modules because dependent modules have changed
* `:long_compilation_threshold` - the timeout (in seconds) after the
`:each_long_compilation` callback is invoked; defaults to `15`
* `:long_compilation_threshold` - the timeout (in seconds) to check for modules
taking too long to compile. For each file that exceeds the threshold, the
`:each_long_compilation` callback is invoked. From Elixir v1.11, only the time
spent compiling the actual module is taken into account by the threshold, the
time spent waiting is not considered. Defaults to `10` seconds.
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
@@ -136,41 +140,47 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(files, output, options) do
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
compiler_pid = self()
:elixir_code_server.cast({:reset_warnings, compiler_pid})
schedulers = max(:erlang.system_info(:schedulers_online), 2)
beam_timestamp = Keyword.get(options, :beam_timestamp)
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = Process.send_after(self(), :threshold_check, threshold)
result =
{outcome, state} =
spawn_workers(files, 0, [], [], %{}, [], %{
dest: Keyword.get(options, :dest),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, []} end),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
beam_timestamp: Keyword.get(options, :beam_timestamp),
long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
profile: Keyword.get(options, :profile),
cycle_start: System.monotonic_time(),
module_counter: 0,
profile: profile_init(Keyword.get(options, :profile)),
output: output,
timer_ref: timer_ref,
long_compilation_threshold: threshold,
schedulers: schedulers
})
# In case --warning-as-errors is enabled and there was a warning,
# compilation status will be set to error.
compilation_status = :elixir_code_server.call({:compilation_status, compiler_pid})
Process.cancel_timer(state.timer_ref)
case {result, compilation_status} do
{{:ok, _, warnings}, :error} ->
receive do
:threshold_check -> :ok
after
0 -> :ok
end
case {outcome, Code.get_compiler_option(:warnings_as_errors)} do
{{:ok, _, [_ | _] = warnings}, true} ->
message = "Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
{:error, warnings, []}
{{:error, errors, warnings}, :error} ->
{{:ok, outcome, warnings}, _} ->
{:ok, write_module_binaries(outcome, output, beam_timestamp), warnings}
{{:error, errors, warnings}, true} ->
{:error, errors ++ warnings, []}
_ ->
result
{{:error, errors, warnings}, _} ->
{:error, errors, warnings}
end
end
@@ -187,6 +197,77 @@ defmodule Kernel.ParallelCompiler do
end
end
defp write_module_binaries(result, {:compile, path}, timestamp) do
Enum.flat_map(result, fn
{{:module, module}, {binary, _map}} ->
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
File.write!(full_path, binary)
if timestamp, do: File.touch!(full_path, timestamp)
[module]
_ ->
[]
end)
end
defp write_module_binaries(result, _output, _timestamp) do
for {{:module, module}, _} <- result, do: module
end
## Verification
defp verify_modules(result, warnings, dependent_modules, state) do
checker_warnings = maybe_check_modules(result, dependent_modules, state)
warnings = Enum.reverse(warnings, checker_warnings)
{{:ok, result, warnings}, state}
end
defp maybe_check_modules(result, runtime_modules, state) do
%{schedulers: schedulers, profile: profile} = state
if :elixir_config.get(:bootstrap) do
[]
else
compiled_modules = checker_compiled_modules(result)
runtime_modules = checker_runtime_modules(runtime_modules)
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
end)
end
end
defp checker_compiled_modules(result) do
for {{:module, _module}, {binary, module_map}} <- result do
{module_map, binary}
end
end
defp checker_runtime_modules(modules) do
for module <- modules,
path = :code.which(module),
is_list(path) and path != [] do
{module, File.read!(path)}
end
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
defp profile_init(nil), do: :none
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
num_modules = length(compiled_modules) + length(runtime_modules)
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
result
end
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
fun.()
end
## Compiler worker spawning
# We already have n=schedulers currently running, don't spawn new ones
defp spawn_workers(
queue,
@@ -203,23 +284,23 @@ defmodule Kernel.ParallelCompiler do
# Release waiting processes
defp spawn_workers([{ref, found} | t], spawned, waiting, files, result, warnings, state) do
waiting =
{files, waiting} =
case List.keytake(waiting, ref, 2) do
{{_kind, pid, ^ref, _on, _defining, _deadlock}, waiting} ->
send(pid, {ref, found})
waiting
{update_timing(files, pid, :waiting), waiting}
nil ->
# In case the waiting process died (for example, it was an async process),
# it will no longer be on the list. So we need to take it into account here.
waiting
{files, waiting}
end
spawn_workers(t, spawned, waiting, files, result, warnings, state)
end
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, state) do
%{output: output, long_compilation_threshold: threshold, dest: dest} = state
%{output: output, dest: dest} = state
parent = self()
file = Path.expand(file)
@@ -242,24 +323,34 @@ defmodule Kernel.ParallelCompiler do
exit(:shutdown)
end)
timer_ref = Process.send_after(self(), {:timed_out, pid}, threshold * 1000)
files = [{pid, ref, file, timer_ref} | files]
file_data = %{
pid: pid,
ref: ref,
file: file,
timestamp: System.monotonic_time(),
compiling: 0,
waiting: 0,
warned: false
}
files = [file_data | files]
spawn_workers(queue, spawned + 1, waiting, files, result, warnings, state)
end
# No more queue, nothing waiting, this cycle is done
defp spawn_workers([], 0, [], [], result, warnings, state) do
cycle_return = each_cycle_return(state.each_cycle.())
state = cycle_timing(result, state)
case each_cycle_return(state.each_cycle.()) do
{:runtime, dependent_modules} ->
write_and_verify_modules(result, warnings, dependent_modules, state)
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, []} ->
write_and_verify_modules(result, warnings, [], state)
{:compile, [], extra_warnings} ->
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more} ->
spawn_workers(more, 0, [], [], result, warnings, state)
{:compile, more, extra_warnings} ->
spawn_workers(more, 0, [], [], result, extra_warnings ++ warnings, state)
end
end
@@ -270,7 +361,7 @@ defmodule Kernel.ParallelCompiler do
[],
1,
[{_, pid, ref, _, _, _}] = waiting,
[{pid, _, _, _}] = files,
[%{pid: pid}] = files,
result,
warnings,
state
@@ -301,7 +392,7 @@ defmodule Kernel.ParallelCompiler do
true ->
errors = handle_deadlock(waiting, files)
{:error, errors, warnings}
{{:error, errors, warnings}, state}
end
end
@@ -327,8 +418,11 @@ defmodule Kernel.ParallelCompiler do
end
end
defp cycle_timing(result, %{profile: :time} = state) do
%{cycle_start: cycle_start, module_counter: module_counter} = state
defp cycle_timing(_result, %{profile: :none} = state) do
state
end
defp cycle_timing(result, %{profile: {:time, cycle_start, module_counter}} = state) do
num_modules = count_modules(result)
diff_modules = num_modules - module_counter
now = System.monotonic_time()
@@ -339,92 +433,25 @@ defmodule Kernel.ParallelCompiler do
"[profile] Finished compilation cycle of #{diff_modules} modules in #{time}ms"
)
%{state | cycle_start: now, module_counter: num_modules}
end
defp cycle_timing(_result, %{profile: nil} = state) do
state
%{state | profile: {:time, now, num_modules}}
end
defp count_modules(result) do
Enum.count(result, &match?({{:module, _}, _}, &1))
end
# TODO: Deprecate on v1.14
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules}
defp each_cycle_return(other), do: other
defp write_and_verify_modules(result, warnings, dependent_modules, state) do
modules = write_module_binaries(result, state)
checker_warnings = maybe_check_modules(result, dependent_modules, state)
warnings = Enum.reverse(warnings, checker_warnings)
{:ok, modules, warnings}
end
defp write_module_binaries(result, %{output: {:compile, path}, beam_timestamp: timestamp}) do
Enum.flat_map(result, fn
{{:module, module}, {binary, _map}} ->
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
File.write!(full_path, binary)
if timestamp, do: File.touch!(full_path, timestamp)
[module]
_ ->
[]
end)
end
defp write_module_binaries(result, _state) do
for {{:module, module}, _} <- result, do: module
end
defp maybe_check_modules(result, runtime_modules, state) do
%{schedulers: schedulers, profile: profile} = state
if :elixir_config.get(:bootstrap) do
[]
else
compiled_modules = checker_compiled_modules(result)
runtime_modules = checker_runtime_modules(runtime_modules)
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
end)
end
end
defp checker_compiled_modules(result) do
for {{:module, _module}, {binary, module_map}} <- result do
{module_map, binary}
end
end
defp checker_runtime_modules(modules) do
for module <- modules,
path = :code.which(module),
is_list(path) do
{module, File.read!(path)}
end
end
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
num_modules = length(compiled_modules) + length(runtime_modules)
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
result
end
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
fun.()
end
# TODO: Deprecate other returns on v1.14
defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings}
defp each_cycle_return({kind, modules}), do: {kind, modules, []}
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules, []}
# The goal of this function is to find leaves in the dependency graph,
# i.e. to find code that depends on code that we know is not being defined.
# Note that not all files have been compile yet, so they may not be in waiting.
defp without_definition(waiting, files) do
nillify_empty(
for {pid, _, _, _} <- files,
{_, ^pid, ref, on, _, _} = List.keyfind(waiting, pid, 1),
for %{pid: pid} <- files,
{_, ^pid, ref, on, _, _} <- List.wrap(List.keyfind(waiting, pid, 1)),
not Enum.any?(waiting, fn {_, _, _, _, defining, _} -> on in defining end),
do: {ref, :not_found}
)
@@ -464,7 +491,6 @@ defmodule Kernel.ParallelCompiler do
for {:module, _, ref, ^module, _defining, _deadlock} <- waiting,
do: {ref, :found}
cancel_waiting_timer(files, child)
result = Map.put(result, {:module, module}, {binary, module_map})
spawn_workers(available ++ queue, spawned, waiting, files, result, warnings, state)
@@ -475,24 +501,33 @@ defmodule Kernel.ParallelCompiler do
{:waiting, kind, child, ref, on, defining, deadlock?} ->
# If we already got what we were waiting for, do not put it on waiting.
# Alternatively, we're waiting on ourselves,
# send :found so that we can crash with a better error.
waiting =
# If we're waiting on ourselves, send :found so that we can crash with
# a better error.
{files, waiting} =
if Map.has_key?(result, {kind, on}) or on in defining do
send(child, {ref, :found})
waiting
{files, waiting}
else
[{kind, child, ref, on, defining, deadlock?} | waiting]
files = update_timing(files, child, :compiling)
{files, [{kind, child, ref, on, defining, deadlock?} | waiting]}
end
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
{:timed_out, child} ->
case List.keyfind(files, child, 0) do
{^child, _, file, _} -> state.each_long_compilation.(file)
_ -> :ok
end
:threshold_check ->
files =
for data <- files do
if data.warned or List.keymember?(waiting, data.pid, 1) do
data
else
data = update_timing(data, :compiling)
data = maybe_warn_long_compilation(data, state)
data
end
end
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
{:warning, file, line, message} ->
@@ -504,10 +539,9 @@ defmodule Kernel.ParallelCompiler do
{:file_ok, child_pid, ref, file, lexical} ->
state.each_file.(file, lexical)
send(child_pid, ref)
cancel_waiting_timer(files, child_pid)
discard_down(child_pid)
new_files = List.keydelete(files, child_pid, 0)
new_files = discard_and_maybe_log_file(files, child_pid, state)
# Sometimes we may have spurious entries in the waiting list
# because someone invoked try/rescue UndefinedFunctionError
@@ -515,28 +549,77 @@ defmodule Kernel.ParallelCompiler do
spawn_workers(queue, spawned - 1, new_waiting, new_files, result, warnings, state)
{:file_cancel, child_pid} ->
cancel_waiting_timer(files, child_pid)
discard_down(child_pid)
new_files = List.keydelete(files, child_pid, 0)
new_files = Enum.reject(files, &(&1.pid == child_pid))
spawn_workers(queue, spawned - 1, waiting, new_files, result, warnings, state)
{:file_error, child_pid, file, {kind, reason, stack}} ->
print_error(file, kind, reason, stack)
cancel_waiting_timer(files, child_pid)
discard_down(child_pid)
files |> List.keydelete(child_pid, 0) |> terminate()
{:error, [to_error(file, kind, reason, stack)], warnings}
files |> Enum.reject(&(&1.pid == child_pid)) |> terminate()
{{:error, [to_error(file, kind, reason, stack)], warnings}, state}
{:DOWN, ref, :process, pid, reason} ->
waiting = List.keydelete(waiting, pid, 1)
case handle_down(files, ref, reason) do
:ok -> wait_for_messages(queue, spawned - 1, waiting, files, result, warnings, state)
{:error, errors} -> {:error, errors, warnings}
{:error, errors} -> {{:error, errors, warnings}, state}
end
end
end
defp update_timing(files, pid, key) do
Enum.map(files, fn data ->
if data.pid == pid do
time = System.monotonic_time()
%{data | key => data[key] + time - data.timestamp, timestamp: time}
else
data
end
end)
end
defp update_timing(data, key) do
time = System.monotonic_time()
%{data | key => data[key] + time - data.timestamp, timestamp: time}
end
defp maybe_warn_long_compilation(data, state) do
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
if not data.warned and compiling >= state.long_compilation_threshold do
state.each_long_compilation.(data.file)
%{data | warned: true}
else
data
end
end
defp discard_and_maybe_log_file(files, pid, state) do
Enum.reject(files, fn data ->
if data.pid == pid do
data = update_timing(data, :compiling)
data = maybe_warn_long_compilation(data, state)
if state.profile != :none do
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
waiting = System.convert_time_unit(data.waiting, :native, :millisecond)
extra = if waiting > 0, do: " (plus #{waiting}ms waiting)", else: ""
IO.puts(
:stderr,
"[profile] #{Path.relative_to_cwd(data.file)} compiled in #{compiling}ms" <> extra
)
end
true
else
false
end
end)
end
defp discard_down(pid) do
receive do
{:DOWN, _, :process, ^pid, _} -> :ok
@@ -548,24 +631,20 @@ defmodule Kernel.ParallelCompiler do
end
defp handle_down(files, ref, reason) do
case List.keyfind(files, ref, 1) do
{child_pid, ^ref, file, _timer_ref} ->
case Enum.find(files, &(&1.ref == ref)) do
%{pid: pid, file: file} ->
print_error(file, :exit, reason, [])
files
|> List.keydelete(child_pid, 0)
|> terminate()
files |> Enum.reject(&(&1.pid == pid)) |> terminate()
{:error, [to_error(file, :exit, reason, [])]}
_ ->
nil ->
:ok
end
end
defp handle_deadlock(waiting, files) do
deadlock =
for {pid, _, file, _} <- files do
for %{pid: pid, file: file} <- files do
{:current_stacktrace, stacktrace} = Process.info(pid, :current_stacktrace)
Process.exit(pid, :kill)
@@ -600,8 +679,8 @@ defmodule Kernel.ParallelCompiler do
end
defp terminate(files) do
for {pid, _, _, _} <- files, do: Process.exit(pid, :kill)
for {pid, _, _, _} <- files, do: discard_down(pid)
for %{pid: pid} <- files, do: Process.exit(pid, :kill)
for %{pid: pid} <- files, do: discard_down(pid)
:ok
end
@@ -612,22 +691,6 @@ defmodule Kernel.ParallelCompiler do
])
end
defp cancel_waiting_timer(files, child_pid) do
case List.keyfind(files, child_pid, 0) do
{^child_pid, _ref, _file, timer_ref} ->
Process.cancel_timer(timer_ref)
# Let's flush the message in case it arrived before we canceled the timeout.
receive do
{:timed_out, ^child_pid} -> :ok
after
0 -> :ok
end
nil ->
:ok
end
end
defp to_error(file, kind, reason, stack) do
line = get_line(file, reason, stack)
file = Path.absname(file)
@@ -645,6 +708,12 @@ defmodule Kernel.ParallelCompiler do
end
end
defp get_line(file, _reason, [{_, _, _, [file: 'expanding macro']}, {_, _, _, info} | _]) do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
Keyword.get(info, :line)
end
end
defp get_line(file, _reason, [{_, _, _, info} | _]) do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
Keyword.get(info, :line)
+25 -21
View File
@@ -14,7 +14,8 @@ defmodule Kernel.SpecialForms do
forms used to define tuple and binary data structures respectively.
This module also documents macros that return information about Elixir's
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0` and `__CALLER__/0`).
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0`,
`__STACKTRACE__/0`, and `__CALLER__/0`).
Additionally, it documents two special forms, `__block__/1` and
`__aliases__/1`, which are not intended to be called directly by the
@@ -186,13 +187,7 @@ defmodule Kernel.SpecialForms do
iex> <<0, "foo">>
<<0, 102, 111, 111>>
Variables or any other type need to be explicitly tagged:
iex> rest = "oo"
iex> <<102, rest>>
** (ArgumentError) argument error
We can solve this by explicitly tagging it as `binary`:
Binaries need to be explicitly tagged as `binary`:
iex> rest = "oo"
iex> <<102, rest::binary>>
@@ -206,6 +201,12 @@ defmodule Kernel.SpecialForms do
iex> <<"foo"::utf32>>
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
Otherwise we get an `ArgumentError` when construcing the binary:
rest = "oo"
<<102, rest>>
** (ArgumentError) argument error
## Options
Many options can be given by using `-` as separator. Order is
@@ -370,7 +371,7 @@ defmodule Kernel.SpecialForms do
defmacro unquote(:<<>>)(args), do: error!([args])
@doc """
Defines a remote call, a call to an anonymous function, or an alias.
Dot operator. Defines a remote call, a call to an anonymous function, or an alias.
The dot (`.`) in Elixir can be used for remote calls:
@@ -437,7 +438,7 @@ defmodule Kernel.SpecialForms do
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Notice we have an inner tuple, containing the atom `:.` representing
Note that we have an inner tuple, containing the atom `:.` representing
the dot as first element:
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
@@ -512,7 +513,7 @@ defmodule Kernel.SpecialForms do
Keyword.values #=> uses MyKeyword.values
Elixir.Keyword.values #=> uses Keyword.values
Notice that calling `alias` without the `:as` option automatically
Note that calling `alias` without the `:as` option automatically
sets an alias based on the last part of the module. For example:
alias Foo.Bar.Baz
@@ -598,7 +599,7 @@ defmodule Kernel.SpecialForms do
## Selector
By default, Elixir imports functions and macros from the given
module, except the ones starting with underscore (which are
module, except the ones starting with an underscore (which are
usually callbacks):
import List
@@ -616,7 +617,7 @@ defmodule Kernel.SpecialForms do
import List, only: [flatten: 1]
import String, except: [split: 2]
Notice that calling `except` is always exclusive on a previously
Note that calling `except` is always exclusive on a previously
declared `import/2`. If there is no previous import, then it applies
to all functions and macros in the module. For example:
@@ -636,7 +637,7 @@ defmodule Kernel.SpecialForms do
## Lexical scope
It is important to notice that `import/2` is lexical. This means you
It is important to note that `import/2` is lexical. This means you
can import specific macros inside specific functions:
defmodule Math do
@@ -720,10 +721,11 @@ defmodule Kernel.SpecialForms do
To retrieve the stacktrace of the current process, use
`Process.info(self(), :current_stacktrace)` instead.
"""
@doc since: "1.7.0"
defmacro __STACKTRACE__, do: error!([])
@doc """
Accesses an already bound variable in match clauses. Also known as the pin operator.
Pin operator. Accesses an already bound variable in match clauses.
## Examples
@@ -755,12 +757,12 @@ defmodule Kernel.SpecialForms do
defmacro ^var, do: error!([var])
@doc """
Matches the value on the right against the pattern on the left.
Match operator. Matches the value on the right against the pattern on the left.
"""
defmacro left = right, do: error!([left, right])
@doc """
Used by types and bitstrings to specify types.
Type operator. Used by types and bitstrings to specify types.
This operator is used in two distinct occasions in Elixir.
It is used in typespecs to specify the type of a variable,
@@ -804,7 +806,7 @@ defmodule Kernel.SpecialForms do
* The first element of the tuple is always an atom or
another tuple in the same representation.
* The second element of the tuple represents [metadata](t:Macro.metadata/0).
* The second element of the tuple represents [metadata](`t:Macro.metadata/0`).
* The third element of the tuple are the arguments for the
function call. The third argument may be an atom, which is
@@ -1062,7 +1064,7 @@ defmodule Kernel.SpecialForms do
Hygiene.no_interference()
#=> %{}
Notice that, even though the alias `M` is not available
Note that, even though the alias `M` is not available
in the context the macro is expanded, the code above works
because `M` still expands to `Map`.
@@ -1557,6 +1559,8 @@ defmodule Kernel.SpecialForms do
@doc """
Defines an anonymous function.
See `Function` for more information.
## Examples
iex> add = fn a, b -> a + b end
@@ -1594,7 +1598,7 @@ defmodule Kernel.SpecialForms do
defmacro unquote(:__block__)(args), do: error!([args])
@doc """
Captures or creates an anonymous function.
Caputure operator. Captures or creates an anonymous function.
## Capture
@@ -1744,7 +1748,7 @@ defmodule Kernel.SpecialForms do
## Variable handling
Notice that variables bound in a clause do not leak to the outer context:
Note that variables bound in a clause do not leak to the outer context:
case data do
{:ok, value} -> value
+20 -7
View File
@@ -22,9 +22,9 @@ defmodule Kernel.Typespec do
{:docs_v1, _, _, _, _, _, docs} ->
for {{:type, name, arity}, _, _, doc, _} <- docs do
case doc do
:none -> {{name, arity}, nil}
:hidden -> {{name, arity}, false}
%{"en" => doc_string} -> {{name, arity}, doc_string}
:hidden -> {{name, arity}, false}
_ -> {{name, arity}, nil}
end
end
@@ -83,7 +83,7 @@ defmodule Kernel.Typespec do
store_typespec(bag, kind, expr, pos)
case :ets.lookup(set, {:function, name, arity}) do
[{{:function, ^name, ^arity}, line, _, doc, doc_meta}] ->
[{{:function, ^name, ^arity}, _, line, _, doc, doc_meta}] ->
store_doc(set, kind, name, arity, line, :doc, doc, doc_meta)
_ ->
@@ -130,11 +130,18 @@ defmodule Kernel.Typespec do
store_typespec(bag, kind, expr, pos)
end
@reserved_signatures [required: 1, optional: 1]
def deftypespec(kind, expr, line, file, module, pos)
when kind in [:type, :typep, :opaque] do
{set, bag} = :elixir_module.data_tables(module)
case type_to_signature(expr) do
{name, arity} = signature when signature in @reserved_signatures ->
compile_error(
:elixir_locals.get_cached_env(pos),
"type #{name}/#{arity} is a reserved type and it cannot be defined"
)
{name, arity} when kind == :typep ->
{line, doc} = get_doc_info(set, :typedoc, line)
@@ -250,7 +257,13 @@ defmodule Kernel.Typespec do
end
if Map.has_key?(type_pairs, type_pair) do
compile_error(env, "type #{name}/#{arity} is already defined")
{error_full_path, error_line} = type_pairs[type_pair]
error_relative_path = Path.relative_to_cwd(error_full_path)
compile_error(
env,
"type #{name}/#{arity} is already defined in #{error_relative_path}:#{error_line}"
)
end
Map.put(type_pairs, type_pair, {file, line})
@@ -464,7 +477,7 @@ defmodule Kernel.Typespec do
_,
state
)
when is_atom(ctx1) and is_atom(ctx2) and is_integer(unit) and unit >= 0 do
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
line = line(meta)
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
end
@@ -489,7 +502,7 @@ defmodule Kernel.Typespec do
state
)
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
is_integer(unit) and size >= 0 and unit >= 0 do
size >= 0 and unit in 1..256 do
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
{{:type, line(meta), :binary, args}, state}
end
@@ -497,7 +510,7 @@ defmodule Kernel.Typespec do
defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
message =
"invalid binary specification, expected <<_::size>>, <<_::_*unit>>, " <>
"or <<_::size, _::_*unit>> with size and unit being non-negative integers"
"or <<_::size, _::_*unit>> with size being non-negative integers, and unit being an integer between 1 and 256"
compile_error(caller, message)
end
+105 -40
View File
@@ -1,8 +1,11 @@
defmodule Keyword do
@moduledoc """
Keyword lists are lists of two-element tuples, where the first
element of the tuple is an atom and the second element can be any
value, used mostly to work with optional values.
A keyword list is a list that consists exclusively of two-element tuples.
The first element of these tuples is known as the *key*, and it must be an atom.
The second element, known as the *value*, can be any term.
Keywords are mostly used to work with optional values.
## Examples
@@ -36,18 +39,32 @@ defmodule Keyword do
in the `Map` module. For example, `Keyword.get/3` will get the first
entry matching the given key, regardless if duplicated entries exist.
Similarly, `Keyword.put/3` and `Keyword.delete/2` ensure all duplicated
entries for a given key are removed when invoked. Note however that
keyword list operations need to traverse the list in order to find
entries for a given key are removed when invoked. Note, however, that
keyword list operations need to traverse the whole list in order to find
keys, so these operations are slower than their map counterparts.
A handful of functions exist to handle duplicated keys, for example,
`get_values/2` returns all values for a given key and `delete_first/2`
deletes just one of the existing entries.
The functions in `Keyword` do not guarantee any property when it comes
to ordering. However, since a keyword list is simply a list, all the
operations defined in `Enum` and `List` can be applied too, especially
when ordering is required.
Even though lists preserve the user ordering, the functions in
`Keyword` do not guarantee any ordering. For example, if you invoke
`Keyword.put(opts, new_key, new_value)`, there is no guarantee to
where `new_key` will be added (to the front, to the end, or
anywhere else).
Given ordering is not guaranteed, it is not recommended to pattern
match on keyword lists either. For example, a function such as:
def my_function([some_key: value, another_key: another_value])
will match
my_function([some_key: :foo, another_key: :bar])
but it won't match
my_function([another_key: :bar, some_key: :foo])
Most of the functions in this module work in linear time. This means
that, the time it takes to perform an operation grows at the same
@@ -55,8 +72,8 @@ defmodule Keyword do
## Call syntax
When keyword lists are passed as the last argument to a function, then
the square brackets around the keyword list can be omitted as well. For
When keyword lists are passed as the last argument to a function,
the square brackets around the keyword list can be omitted. For
example, the keyword list syntax:
String.split("1-0", "-", [trim: true, parts: 2])
@@ -242,13 +259,13 @@ defmodule Keyword do
Gets the value from `key` and updates it, all in one pass.
This `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the "get" value
is not present) and must return a two-element tuple: the current value
(the retrieved value, which can be operated on before being returned)
and the new value to be stored under `key`. The `fun` may also
return `:pop`, implying the current value shall be removed from the
keyword list and returned.
The returned value is a tuple with the "get" value returned by
The returned value is a tuple with the current value returned by
`fun` and a new keyword list with the updated value under `key`.
## Examples
@@ -270,7 +287,9 @@ defmodule Keyword do
{nil, [a: 1]}
"""
@spec get_and_update(t, key, (value -> {get, value} | :pop)) :: {get, t} when get: term
@spec get_and_update(t, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, value}
when current_value: value
def get_and_update(keywords, key, fun)
when is_list(keywords) and is_atom(key),
do: get_and_update(keywords, [], key, fun)
@@ -307,11 +326,11 @@ defmodule Keyword do
Gets the value from `key` and updates it. Raises if there is no `key`.
This `fun` argument receives the value of `key` and must return a
two-element tuple: the "get" value (the retrieved value, which can be
two-element tuple: the current value (the retrieved value, which can be
operated on before being returned) and the new value to be stored under
`key`.
The returned value is a tuple with the "get" value returned by `fun` and a new
The returned value is a tuple with the current value returned by `fun` and a new
keyword list with the updated value under `key`.
## Examples
@@ -332,7 +351,9 @@ defmodule Keyword do
{1, []}
"""
@spec get_and_update!(t, key, (value -> {get, value})) :: {get, t} when get: term
@spec get_and_update!(t, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, t}
when current_value: value
def get_and_update!(keywords, key, fun) do
get_and_update!(keywords, key, fun, [])
end
@@ -431,13 +452,30 @@ defmodule Keyword do
iex> Keyword.keys(a: 1, b: 2)
[:a, :b]
iex> Keyword.keys(a: 1, b: 2, a: 3)
[:a, :b, :a]
iex> Keyword.keys([{:a, 1}, {"b", 2}, {:c, 3}])
** (ArgumentError) expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, got: {"b", 2}
"""
@spec keys(t) :: [key]
def keys(keywords) when is_list(keywords) do
:lists.map(fn {k, _} -> k end, keywords)
try do
:lists.map(
fn
{key, _} when is_atom(key) -> key
element -> throw(element)
end,
keywords
)
catch
element ->
raise ArgumentError,
"expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, " <>
"got: #{inspect(element)}"
end
end
@doc """
@@ -612,18 +650,41 @@ defmodule Keyword do
end
end
@doc false
@deprecated "Use Keyword.fetch/2 + Keyword.put/3 instead"
@doc """
Puts a value under `key` only if the `key` already exists in `keywords`.
In the case a value is stored multiple times in the keyword list,
later occurrences are removed.
## Examples
iex> Keyword.replace([a: 1, b: 2, a: 4], :a, 3)
[a: 3, b: 2]
iex> Keyword.replace([a: 1], :b, 2)
[a: 1]
"""
@doc since: "1.11.0"
@spec replace(t, key, value) :: t
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, _} -> [{key, value} | delete(keywords, key)]
false -> keywords
end
do_replace(keywords, key, value)
end
defp do_replace([{key, _} | keywords], key, value) do
[{key, value} | delete(keywords, key)]
end
defp do_replace([{_, _} = e | keywords], key, value) do
[e | do_replace(keywords, key, value)]
end
defp do_replace([], _key, _value) do
[]
end
@doc """
Alters the value stored under `key` to `value`, but only
if the entry `key` already exists in `keywords`.
Puts a value under `key` only if the `key` already exists in `keywords`.
If `key` is not present in `keywords`, a `KeyError` exception is raised.
@@ -652,7 +713,7 @@ defmodule Keyword do
[e | replace!(keywords, key, value, original)]
end
defp replace!([], key, _value, original) when is_atom(key) do
defp replace!([], key, _value, original) do
raise(KeyError, key: key, term: original)
end
@@ -822,7 +883,7 @@ defmodule Keyword do
** (KeyError) key :b not found in: [a: 1]
"""
@spec update!(t, key, (value -> value)) :: t
@spec update!(t, key, (current_value :: value -> new_value :: value)) :: t
def update!(keywords, key, fun)
when is_list(keywords) and is_atom(key) and is_function(fun, 1) do
update!(keywords, key, fun, keywords)
@@ -843,34 +904,38 @@ defmodule Keyword do
@doc """
Updates the `key` in `keywords` with the given function.
If the `key` does not exist, inserts the given `initial` value.
If the `key` does not exist, it inserts the given `default` value.
If there are duplicated keys, they are all removed and only the first one
is updated.
The default value will not be passed through the update function.
## Examples
iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
iex> Keyword.update([a: 1], :a, 13, fn existing_value -> existing_value * 2 end)
[a: 2]
iex> Keyword.update([a: 1, a: 2], :a, 13, &(&1 * 2))
iex> Keyword.update([a: 1, a: 2], :a, 13, fn existing_value -> existing_value * 2 end)
[a: 2]
iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
iex> Keyword.update([a: 1], :b, 11, fn existing_value -> existing_value * 2 end)
[a: 1, b: 11]
"""
@spec update(t, key, value, (value -> value)) :: t
def update(keywords, key, initial, fun)
@spec update(t, key, default :: value, (existing_value :: value -> updated_value :: value)) :: t
def update(keywords, key, default, fun)
def update([{key, value} | keywords], key, _initial, fun) do
def update([{key, value} | keywords], key, _default, fun) do
[{key, fun.(value)} | delete(keywords, key)]
end
def update([{_, _} = e | keywords], key, initial, fun) do
[e | update(keywords, key, initial, fun)]
def update([{_, _} = e | keywords], key, default, fun) do
[e | update(keywords, key, default, fun)]
end
def update([], key, initial, _fun) when is_atom(key) do
[{key, initial}]
def update([], key, default, _fun) when is_atom(key) do
[{key, default}]
end
@doc """
@@ -974,7 +1039,7 @@ defmodule Keyword do
end
@doc """
Returns the first value for `key` and removes all associated antries in the keyword list,
Returns the first value for `key` and removes all associated entries in the keyword list,
raising if `key` is not present.
This function behaves like `pop/3`, but raises in cases the `key` is not present in the
+3 -3
View File
@@ -1,6 +1,6 @@
defmodule List do
@moduledoc """
Linked lists hold zero, one, or more elements in the choosen order.
Linked lists hold zero, one, or more elements in the chosen order.
Lists in Elixir are specified between square brackets:
@@ -897,7 +897,7 @@ defmodule List do
* integers representing Unicode code points
* a list containing one of these three elements
Notice that this function expects a list of integers representing
Note that this function expects a list of integers representing
Unicode code points. If you have a list of bytes, you must instead use
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
@@ -952,7 +952,7 @@ defmodule List do
Converts a list of integers representing Unicode code points, lists or
strings into a charlist.
Notice that this function expects a list of integers representing
Note that this function expects a list of integers representing
Unicode code points. If you have a list of bytes, you must instead use
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
+1 -1
View File
@@ -3,7 +3,7 @@ defprotocol List.Chars do
The `List.Chars` protocol is responsible for
converting a structure to a charlist (only if applicable).
The only function required to be implemented is
The only function that must be implemented is
`to_charlist/1` which does the conversion.
The `to_charlist/1` function automatically imported
+23 -17
View File
@@ -14,7 +14,7 @@ defmodule Macro do
end
def fun_inspect(value) do
IO.inpect(value)
IO.inspect(value)
value
end
end
@@ -32,7 +32,7 @@ defmodule Macro do
#=> 1
So far they behave the same, as we are passing an integer as argument.
But what happens when we pass an expresion:
But what happens when we pass an expression:
macro_inspect(1 + 2)
#=> {:+, [line: 3], [1, 2]}
@@ -163,8 +163,9 @@ defmodule Macro do
with parens. The `:closing` does not delimit the end of expression if
there are `:do` and `:end` metadata (when `:token_metadata` is true)
* `:column` - the column number of the AST node (when `:columns` is true)
* `:delimiter` - contains the opening delimiter for sigils, strings, atoms,
and charlists as a string (such as `"{"`, `"/"`, `":"`, and the like)
* `:delimiter` - contains the opening delimiter for sigils, strings,
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
* `:format` - set to `:keyword` when an atom is defined as a keyword
* `:do` - contains metadata about the `do` location in a function call with
`do/end` blocks (when `:token_metadata` is true)
* `:end` - contains metadata about the `end` location in a function call with
@@ -172,6 +173,7 @@ defmodule Macro do
* `:end_of_expression` - denotes when the end of expression effectively
happens. Available for all expressions except the last one inside a
`__block__` (when `:token_metadata` is true)
* `:indentation` - indentation of a sigil heredoc
The following metadata keys are private:
@@ -531,7 +533,7 @@ defmodule Macro do
As an example, `ExUnit` stores the AST of every assertion, so when
an assertion fails we can show code snippets to users. Without this
option, each time the test module is compiled, we get a different
MD5 of the module byte code, because the AST contains metadata,
MD5 of the module bytecode, because the AST contains metadata,
such as counters, specific to the compilation environment. By pruning
the metadata, we ensure that the module is deterministic and reduce
the amount of data `ExUnit` needs to keep around.
@@ -574,7 +576,7 @@ defmodule Macro do
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is even capable of
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
It will raise `CompileError` if the struct is not available.
@@ -894,13 +896,13 @@ defmodule Macro do
end
# All other calls
def to_string({target, meta, []} = ast, fun) do
target = call_to_string(target, fun)
def to_string({{:., _, [left, _]} = target, meta, []} = ast, fun) do
to_string = call_to_string(target, fun)
if meta[:no_parens] do
fun.(ast, target)
if is_tuple(left) && meta[:no_parens] do
fun.(ast, to_string)
else
fun.(ast, target <> "()")
fun.(ast, to_string <> "()")
end
end
@@ -961,10 +963,14 @@ defmodule Macro do
Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)
end
defp bitpart_to_string({:"::", _, [left, right]} = ast, fun) do
defp bitpart_to_string({:"::", meta, [left, right]} = ast, fun) do
result =
op_to_string(left, fun, :"::", :left) <>
"::" <> bitmods_to_string(right, fun, :"::", :right)
if meta[:inferred_bitstring_spec] do
to_string(left, fun)
else
op_to_string(left, fun, :"::", :left) <>
"::" <> bitmods_to_string(right, fun, :"::", :right)
end
fun.(ast, result)
end
@@ -1021,11 +1027,11 @@ defmodule Macro do
binary when is_binary(binary) ->
binary = inspect_no_limit(binary)
binary_part(binary, 1, byte_size(binary) - 2)
binary = binary_part(binary, 1, byte_size(binary) - 2)
escape_sigil(binary, left)
end)
escaped = escape_sigil(parts, left)
<<left::binary, escaped::binary, right::binary>>
<<left::binary, parts::binary, right::binary>>
end
defp escape_sigil(parts, "("), do: String.replace(parts, ")", ~S"\)")
+68 -47
View File
@@ -37,15 +37,19 @@ defmodule Map do
iex> map["non_existing_key"]
nil
For accessing atom keys, one may also `map.key`. Note that while `map[key]` will
return `nil` if `map` doesn't contain `key`, `map.key` will raise if `map` doesn't
contain the key `:key`.
To access atom keys, one may also use the `map.key` notation. Note that `map.key`
will raise a `KeyError` if the `map` doesn't contain the key `:key`, compared to
`map[:key]`, that would return `nil`.
iex> map = %{foo: "bar", baz: "bong"}
iex> map.foo
"bar"
iex> map.non_existing_key
** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
map = %{foo: "bar", baz: "bong"}
map.foo
#=> "bar"
map.non_existing_key
#=> ** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
> Note: do not add parens when accessing fields, such as in `data.key()`.
> If parenthesis are used, Elixir will consider it to be a function call
> on `data`, which would be expected to be an atom.
The two syntaxes for accessing keys reveal the dual nature of maps. The `map[key]`
syntax is used for dynamically created maps that may have any key, of any type.
@@ -63,8 +67,10 @@ defmodule Map do
iex> %{a: a} = %{:a => 1, "b" => 2, [:c, :e, :e] => 3}
iex> a
1
iex> %{:c => 3} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
But this will raise a `MatchError` exception:
%{:c => 3} = %{:a => 1, 2 => :b}
Variables can be used as map keys both when writing map literals as well as
when matching:
@@ -82,8 +88,10 @@ defmodule Map do
iex> map = %{one: 1, two: 2}
iex> %{map | one: "one"}
%{one: "one", two: 2}
iex> %{map | three: 3}
** (KeyError) key :three not found
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
%{map | three: 3}
The functions in this module that need to find a specific key work in logarithmic time.
This means that the time it takes to find keys grows as the map grows, but it's not
@@ -236,8 +244,8 @@ defmodule Map do
@doc """
Fetches the value for a specific `key` in the given `map`.
If `map` contains the given `key` with value `value`, then `{:ok, value}` is
returned. If `map` doesn't contain `key`, `:error` is returned.
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
If `map` doesn't contain `key`, `:error` is returned.
Inlined by the compiler.
@@ -256,7 +264,7 @@ defmodule Map do
Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`.
If `map` contains the given `key`, the corresponding value is returned. If
If `map` contains `key`, the corresponding value is returned. If
`map` doesn't contain `key`, a `KeyError` exception is raised.
Inlined by the compiler.
@@ -265,8 +273,6 @@ defmodule Map do
iex> Map.fetch!(%{a: 1}, :a)
1
iex> Map.fetch!(%{a: 1}, :b)
** (KeyError) key :b not found in: %{a: 1}
"""
@spec fetch!(map, key) :: value
@@ -300,8 +306,20 @@ defmodule Map do
end
end
@doc false
@deprecated "Use Map.fetch/2 + Map.put/3 instead"
@doc """
Puts a value under `key` only if the `key` already exists in `map`.
## Examples
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace(%{a: 1}, :b, 2)
%{a: 1}
"""
@doc since: "1.11.0"
@spec replace(map, key, value) :: map
def replace(map, key, value) do
case map do
%{^key => _value} ->
@@ -316,8 +334,7 @@ defmodule Map do
end
@doc """
Alters the value stored under `key` to `value`, but only
if the entry `key` already exists in `map`.
Puts a value under `key` only if the `key` already exists in `map`.
If `key` is not present in `map`, a `KeyError` exception is raised.
@@ -422,7 +439,7 @@ defmodule Map do
@doc """
Gets the value for a specific `key` in `map`.
If `key` is present in `map` with value `value`, then `value` is
If `key` is present in `map` then its value `value` is
returned. Otherwise, `default` is returned.
If `default` is not provided, `nil` is used.
@@ -456,7 +473,7 @@ defmodule Map do
@doc """
Gets the value for a specific `key` in `map`.
If `key` is present in `map` with value `value`, then `value` is
If `key` is present in `map` then its value `value` is
returned. Otherwise, `fun` is evaluated and its result is returned.
This is useful if the default value is very expensive to calculate or
@@ -584,38 +601,39 @@ defmodule Map do
@doc """
Updates the `key` in `map` with the given function.
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
not present in `map`, `initial` is inserted as the value of `key`. The initial
If `key` is present in `map` then the existing value is passed to `fun` and its result is
used as the updated value of `key`. If `key` is
not present in `map`, `default` is inserted as the value of `key`. The default
value will not be passed through the update function.
## Examples
iex> Map.update(%{a: 1}, :a, 13, &(&1 * 2))
iex> Map.update(%{a: 1}, :a, 13, fn existing_value -> existing_value * 2 end)
%{a: 2}
iex> Map.update(%{a: 1}, :b, 11, &(&1 * 2))
iex> Map.update(%{a: 1}, :b, 11, fn existing_value -> existing_value * 2 end)
%{a: 1, b: 11}
"""
@spec update(map, key, value, (value -> value)) :: map
def update(map, key, initial, fun) when is_function(fun, 1) do
@spec update(map, key, default :: value, (existing_value :: value -> updated_value :: value)) ::
map
def update(map, key, default, fun) when is_function(fun, 1) do
case map do
%{^key => value} ->
put(map, key, fun.(value))
%{} ->
put(map, key, initial)
put(map, key, default)
other ->
:erlang.error({:badmap, other}, [map, key, initial, fun])
:erlang.error({:badmap, other}, [map, key, default, fun])
end
end
@doc """
Returns and removes the value associated with `key` in `map`.
Removes the value associated with `key` in `map` and returns the value and the updated map.
If `key` is present in `map` with value `value`, `{value, new_map}` is
returned where `new_map` is the result of removing `key` from `map`. If `key`
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
the key and `new_map` is the result of removing `key` from `map`. If `key`
is not present in `map`, `{default, map}` is returned.
## Examples
@@ -628,7 +646,7 @@ defmodule Map do
{3, %{a: 1}}
"""
@spec pop(map, key, value) :: {value, map}
@spec pop(map, key, value) :: {value, new_map :: map}
def pop(map, key, default \\ nil) do
case :maps.take(key, map) do
{_, _} = tuple -> tuple
@@ -664,8 +682,8 @@ defmodule Map do
@doc """
Lazily returns and removes the value associated with `key` in `map`.
If `key` is present in `map` with value `value`, `{value, new_map}` is
returned where `new_map` is the result of removing `key` from `map`. If `key`
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
the key and `new_map` is the result of removing `key` from `map`. If `key`
is not present in `map`, `{fun_result, map}` is returned, where `fun_result`
is the result of applying `fun`.
@@ -781,8 +799,8 @@ defmodule Map do
@doc """
Updates `key` with the given function.
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
If `key` is present in `map` then the existing value is passed to `fun` and its result is
used as the updated value of `key`. If `key` is
not present in `map`, a `KeyError` exception is raised.
## Examples
@@ -794,7 +812,7 @@ defmodule Map do
** (KeyError) key :b not found in: %{a: 1}
"""
@spec update!(map, key, (value -> value)) :: map
@spec update!(map, key, (existing_value :: value -> updated_value :: value)) :: map
def update!(map, key, fun) when is_function(fun, 1) do
value = fetch!(map, key)
put(map, key, fun.(value))
@@ -804,13 +822,13 @@ defmodule Map do
Gets the value from `key` and updates it, all in one pass.
`fun` is called with the current value under `key` in `map` (or `nil` if `key`
is not present in `map`) and must return a two-element tuple: the "get" value
is not present in `map`) and must return a two-element tuple: the current value
(the retrieved value, which can be operated on before being returned) and the
new value to be stored under `key` in the resulting new map. `fun` may also
return `:pop`, which means the current value shall be removed from `map` and
returned (making this function behave like `Map.pop(map, key)`).
The returned value is a tuple with the "get" value returned by
The returned value is a two-element tuple with the current value returned by
`fun` and a new map with the updated value under `key`.
## Examples
@@ -832,7 +850,9 @@ defmodule Map do
{nil, %{a: 1}}
"""
@spec get_and_update(map, key, (value -> {get, value} | :pop)) :: {get, map} when get: term
@spec get_and_update(map, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, map}
when current_value: value
def get_and_update(map, key, fun) when is_function(fun, 1) do
current = get(map, key)
@@ -849,7 +869,7 @@ defmodule Map do
end
@doc """
Gets the value from `key` and updates it. Raises if there is no `key`.
Gets the value from `key` and updates it, all in one pass. Raises if there is no `key`.
Behaves exactly like `get_and_update/3`, but raises a `KeyError` exception if
`key` is not present in `map`.
@@ -872,8 +892,9 @@ defmodule Map do
{1, %{}}
"""
@spec get_and_update!(map, key, (value -> {get, value} | :pop)) :: {get, map}
when get: term
@spec get_and_update!(map, key, (value -> {current_value, new_value :: value} | :pop)) ::
{current_value, map}
when current_value: value
def get_and_update!(map, key, fun) when is_function(fun, 1) do
value = fetch!(map, key)
+1 -1
View File
@@ -238,7 +238,7 @@ defmodule MapSet do
Map.equal?(map1, map2)
end
# Elixir v1.5 change the map representation, so on
# Elixir v1.5 changed the map representation, so on
# version mismatch we need to compare the keys directly.
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
map_size(map1) == map_size(map2) and all_in?(map1, map2)
+41 -21
View File
@@ -57,7 +57,7 @@ defmodule Module do
one of the callbacks, a warning will be raised.
For detailed documentation, see the
[behaviour typespec documentation](typespecs.html#behaviours).
[behaviour typespec documentation](typespecs.md#behaviours).
### `@impl`
@@ -248,8 +248,13 @@ defmodule Module do
attribute allows the module to annotate which external resources
have been used.
Tools like Mix may use this information to ensure the module is
recompiled in case any of the external resources change.
Tools may use this information to ensure the module is recompiled
in case any of the external resources change, see for example:
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
If the external resource does not exist, the module still has
a dependency on it, causing the module be recompiled as soon
as the file is added.
### `@file`
@@ -345,6 +350,8 @@ defmodule Module do
behaviour callbacks are optional
* `@impl` - declares an implementation of a callback function or macro
For detailed documentation, see the [typespec documentation](typespecs.md).
### Custom attributes
In addition to the built-in attributes outlined above, custom attributes may
@@ -499,8 +506,6 @@ defmodule Module do
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
the given module or the given `Mod.fun/arity` are not defined
You can see a handful more options used by the Erlang compiler in
the documentation for the [`:compile` module](http://www.erlang.org/doc/man/compile.html).
'''
@typep definition :: {atom, arity}
@@ -625,7 +630,7 @@ defmodule Module do
It returns a tuple of shape `{:module, module, binary, term}`
where `module` is the module name, `binary` is the module
byte code and `term` is the result of the last expression in
bytecode and `term` is the result of the last expression in
`quoted`.
Similar to `Kernel.defmodule/2`, the binary will only be
@@ -1049,11 +1054,11 @@ defmodule Module do
"""
@spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, def_kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
def definitions_in(module, kind)
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
assert_not_compiled!(__ENV__.function, module, @extra_error_msg_definitions_in)
{set, _} = data_tables_for(module)
:lists.concat(:ets.match(set, {{:def, :"$1"}, def_kind, :_, :_, :_, :_}))
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
end
@doc """
@@ -1397,7 +1402,7 @@ defmodule Module do
if doc, do: {:error, :private_doc}, else: :ok
else
{set, _bag} = data_tables_for(module)
compile_doc(set, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
compile_doc(set, nil, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
:ok
end
end
@@ -1410,16 +1415,17 @@ defmodule Module do
{set, bag} = data_tables_for(module)
{arity, defaults} = args_count(args, 0, 0)
impl = compile_impl(set, bag, name, env, kind, arity, defaults)
context = Keyword.get(:ets.lookup_element(set, {:def, {name, arity}}, 3), :context)
impl = compile_impl(set, bag, context, name, env, kind, arity, defaults)
doc_meta = compile_doc_meta(set, bag, name, arity, defaults)
{line, doc} = get_doc_info(set, env)
compile_doc(set, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
compile_doc(set, context, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
:ok
end
defp compile_doc(_table, line, kind, name, arity, _args, _body, doc, _doc_meta, env, _impl)
defp compile_doc(_table, _ctx, line, kind, name, arity, _args, _body, doc, _meta, env, _impl)
when kind in [:defp, :defmacrop] do
if doc do
message =
@@ -1430,21 +1436,37 @@ defmodule Module do
end
end
defp compile_doc(table, line, kind, name, arity, args, _body, doc, doc_meta, env, impl) do
defp compile_doc(table, ctx, line, kind, name, arity, args, body, doc, doc_meta, env, impl) do
key = {doc_key(kind), name, arity}
signature = build_signature(args, env)
case :ets.lookup(table, key) do
[] ->
doc = if is_nil(doc) && impl, do: false, else: doc
:ets.insert(table, {key, line, signature, doc, doc_meta})
:ets.insert(table, {key, ctx, line, signature, doc, doc_meta})
[{_, current_ctx, current_line, current_sign, current_doc, current_doc_meta}] ->
if is_binary(current_doc) and is_binary(doc) and body != nil and is_nil(current_ctx) do
message = ~s'''
redefining @doc attribute previously set at line #{current_line}.
Please remove the duplicate docs. If instead you want to override a \
previously defined @doc, attach the @doc attribute to a function head:
@doc """
new docs
"""
def #{name}(...)
'''
IO.warn(message, Macro.Env.stacktrace(%{env | line: line}))
end
[{_, current_line, current_sign, current_doc, current_doc_meta}] ->
signature = merge_signatures(current_sign, signature, 1)
doc = if is_nil(doc), do: current_doc, else: doc
doc = if is_nil(doc) && impl, do: false, else: doc
doc_meta = Map.merge(current_doc_meta, doc_meta)
:ets.insert(table, {key, current_line, signature, doc, doc_meta})
:ets.insert(table, {key, ctx, current_line, signature, doc, doc_meta})
end
end
@@ -1492,14 +1514,12 @@ defmodule Module do
defp deprecated_reason(name, arity, reason),
do: {:deprecated, {{name, arity}, reason}}
defp compile_impl(set, bag, name, env, kind, arity, defaults) do
defp compile_impl(set, bag, context, name, env, kind, arity, defaults) do
%{line: line, file: file} = env
case :ets.take(set, :impl) do
[{:impl, value, _}] ->
pair = {name, arity}
meta = :ets.lookup_element(set, {:def, pair}, 3)
impl = {pair, Keyword.get(meta, :context), defaults, kind, line, file, value}
impl = {{name, arity}, context, defaults, kind, line, file, value}
:ets.insert(bag, {:impls, impl})
value
-315
View File
@@ -1,315 +0,0 @@
defmodule Module.Checker do
alias Module.ParallelChecker
@moduledoc false
def verify(module, cache) do
case prepare_module(module) do
{:ok, map} ->
undefined_and_deprecation_warnings = undefined_and_deprecation_warnings(map, cache)
infer_warnings = infer_definitions(map)
warnings = infer_warnings ++ undefined_and_deprecation_warnings
emit_warnings(warnings)
:error ->
[]
end
end
defp prepare_module({module, module_map}) when is_map(module_map) do
{:ok,
%{
module: module,
file: module_map.file,
definitions: module_map.definitions,
deprecated: module_map.deprecated,
no_warn_undefined: no_warn_undefined(module_map.compile_opts)
}}
end
defp prepare_module({module, binary}) when is_binary(binary) do
with {:ok, debug_info} <- debug_info(module, binary),
{:ok, checker_info} <- checker_chunk(binary) do
{:ok,
%{
module: module,
file: debug_info.file,
definitions: debug_info.definitions,
deprecated: checker_info.deprecated,
no_warn_undefined: checker_info.no_warn_undefined
}}
end
end
defp no_warn_undefined(compile_opts) do
for(
{:no_warn_undefined, values} <- compile_opts,
value <- List.wrap(values),
do: value
)
end
defp debug_info(module, binary) do
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} <- chunk,
{:ok, info} <- backend.debug_info(:elixir_v1, module, data, []) do
{:ok, %{definitions: info.definitions, file: info.relative_file}}
else
_ -> :error
end
end
defp checker_chunk(binary) do
with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
deprecated = Enum.map(contents.exports, fn {fun, map} -> {fun, map.deprecated_reason} end)
{:ok, %{deprecated: deprecated, no_warn_undefined: contents.no_warn_undefined}}
else
_ -> :error
end
end
defp infer_definitions(map) do
results = Module.Types.infer_definitions(map.file, map.module, map.definitions)
Enum.flat_map(results, fn {_function, reasons} -> reasons end)
end
defp undefined_and_deprecation_warnings(map, cache) do
state = %{
cache: cache,
file: map.file,
module: map.module,
no_warn_undefined: merge_no_warn_undefined(map),
function: nil,
warnings: []
}
state = check_definitions(map.definitions, state)
state.warnings
|> merge_warnings()
|> sort_warnings()
end
defp merge_no_warn_undefined(map) do
case Code.get_compiler_option(:no_warn_undefined) do
:all ->
:all
list when is_list(list) ->
map.no_warn_undefined ++ list
end
end
defp check_definitions(definitions, state) do
Enum.reduce(definitions, state, &check_definition/2)
end
defp check_definition({function, _kind, meta, clauses}, state) do
with_file_meta(%{state | function: function}, meta, fn state ->
Enum.reduce(clauses, state, &check_clause/2)
end)
end
defp with_file_meta(%{file: original_file} = state, meta, fun) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} ->
state = fun.(%{state | file: meta_file})
%{state | file: original_file}
:error ->
fun.(state)
end
end
defp check_clause({_meta, args, _guards, body}, state) do
state = check_expr(args, state)
check_expr(body, state)
end
# &Mod.fun/arity
defp check_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
when is_atom(module) and is_atom(fun) do
check_remote(module, fun, arity, meta, state)
end
# Mod.fun(...)
defp check_expr({{:., meta, [module, fun]}, _, args}, state)
when is_atom(module) and is_atom(fun) do
state = check_remote(module, fun, length(args), meta, state)
check_expr(args, state)
end
# %Module{...}
defp check_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
when is_atom(module) and is_list(args) do
state = check_remote(module, :__struct__, 0, meta, state)
check_expr(args, state)
end
# Function call
defp check_expr({left, _meta, right}, state) when is_list(right) do
state = check_expr(right, state)
check_expr(left, state)
end
# {x, y}
defp check_expr({left, right}, state) do
state = check_expr(right, state)
check_expr(left, state)
end
# [...]
defp check_expr(list, state) when is_list(list) do
Enum.reduce(list, state, &check_expr/2)
end
defp check_expr(_other, state) do
state
end
defp check_remote(module, fun, arity, meta, state) do
# TODO: In the future we may want to warn for modules defined
# in the local context
if Keyword.get(meta, :context_module, false) and state.module != module do
state
else
ParallelChecker.preload_module(state.cache, module)
check_export(module, fun, arity, meta, state)
end
end
defp check_export(module, fun, arity, meta, state) do
case ParallelChecker.fetch_export(state.cache, module, fun, arity) do
{:ok, :def, reason} ->
check_deprecated(module, fun, arity, reason, meta, state)
{:ok, :defmacro, reason} ->
state = warn(meta, state, {:unrequired_module, module, fun, arity})
check_deprecated(module, fun, arity, reason, meta, state)
{:error, :module} ->
if warn_undefined?(module, fun, arity, state) do
warn(meta, state, {:undefined_module, module, fun, arity})
else
state
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, state) do
exports = ParallelChecker.all_exports(state.cache, module)
warn(meta, state, {:undefined_function, module, fun, arity, exports})
else
state
end
end
end
defp check_deprecated(module, fun, arity, reason, meta, state) do
if reason do
warn(meta, state, {:deprecated, module, fun, arity, reason})
else
state
end
end
# TODO: Do not warn inside guards
# TODO: Properly handle protocols
defp warn_undefined?(_module, :__impl__, 1, _state), do: false
defp warn_undefined?(:erlang, :orelse, 2, _state), do: false
defp warn_undefined?(:erlang, :andalso, 2, _state), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, state) do
not Enum.any?(state.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
defp warn(meta, state, warning) do
{fun, arity} = state.function
location = {state.file, meta[:line], {state.module, fun, arity}}
%{state | warnings: [{__MODULE__, warning, location} | state.warnings]}
end
defp merge_warnings(warnings) do
Enum.reduce(warnings, %{}, fn {module, warning, location}, acc ->
locations = MapSet.new([location])
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
end)
end
defp sort_warnings(warnings) do
warnings
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|> Enum.sort()
end
defp emit_warnings(warnings) do
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
print_warning([message, ?\n, format_locations(locations)])
Enum.map(locations, fn {file, line, _mfa} ->
{file, line, message}
end)
end)
end
def format_warning({:undefined_module, module, fun, arity}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_warning({:undefined_function, module, fun, arity, exports}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]
end
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_warning({:unrequired_module, module, fun, arity}) do
[
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]
end
defp format_locations([location]) do
format_location(location)
end
defp format_locations(locations) do
[
"Found at #{length(locations)} locations:\n",
Enum.map(locations, &format_location/1)
]
end
defp format_location({file, line, {module, fun, arity}}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line), ": "], else: []
mfa = Exception.format_mfa(module, fun, arity)
[" ", file, ?:, line, mfa, ?\n]
end
defp print_warning(message) do
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
end
end
+1 -1
View File
@@ -137,7 +137,7 @@ defmodule Module.LocalsTracker do
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private micros are unreachable unless they have been
# All private macros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
end
+150 -13
View File
@@ -10,17 +10,24 @@ defmodule Module.ParallelChecker do
the modules and adds the ExCk chunk to the binaries. Returns the updated
binaries and a list of warnings from the verification.
"""
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer()) :: [warning()]
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer() | nil) :: [warning()]
def verify(compiled_modules, runtime_binaries, schedulers \\ nil) do
compiled_maps = Enum.map(compiled_modules, fn {map, _binary} -> {map.module, map} end)
check_modules = compiled_maps ++ runtime_binaries
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
{:ok, server} = :gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
preload_cache(get_ets(server), check_modules)
start(server)
case compiled_maps ++ runtime_binaries do
[] ->
[]
collect_results(length(check_modules), [])
check_modules ->
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
{:ok, server} =
:gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
preload_cache(get_ets(server), check_modules)
start(server)
collect_results(length(check_modules), [])
end
end
defp collect_results(0, warnings) do
@@ -34,6 +41,14 @@ defmodule Module.ParallelChecker do
end
end
@doc """
Test cache.
"""
def test_cache do
{:ok, pid} = :gen_server.start_link(__MODULE__, [[], self(), 1], [])
{pid, get_ets(pid)}
end
@doc """
Preloads a module into the cache. Call this function before any other
cache lookups for the module.
@@ -81,6 +96,116 @@ defmodule Module.ParallelChecker do
|> Enum.sort()
end
## Module checking
defp check_module(module, cache) do
case extract_definitions(module) do
{:ok, module, file, definitions, no_warn_undefined} ->
Module.Types.warnings(module, file, definitions, no_warn_undefined, cache)
|> group_warnings()
|> emit_warnings()
:error ->
[]
end
end
defp extract_definitions({module, module_map}) when is_map(module_map) do
no_warn_undefined =
module_map.compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
{:ok, module, module_map.file, module_map.definitions, no_warn_undefined}
end
defp extract_definitions({module, binary}) when is_binary(binary) do
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} <- chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
extract_definitions({module, module_map})
else
_ -> :error
end
end
defp extract_no_warn_undefined(compile_opts) do
for(
{:no_warn_undefined, values} <- compile_opts,
value <- List.wrap(values),
do: value
)
end
defp merge_compiler_no_warn_undefined(no_warn_undefined) do
case Code.get_compiler_option(:no_warn_undefined) do
:all ->
:all
list when is_list(list) ->
no_warn_undefined ++ list
end
end
## Warning helpers
def group_warnings(warnings) do
warnings
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
locations = MapSet.new([location])
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
end)
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|> Enum.sort()
end
def emit_warnings(warnings) do
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
print_warning([message, ?\n, format_locations(locations)])
Enum.map(locations, fn {file, line, _mfa} ->
{file, line, message}
end)
end)
end
defp format_locations([location]) do
format_location(location)
end
defp format_locations(locations) do
[
"Found at #{length(locations)} locations:\n",
Enum.map(locations, &format_location/1)
]
end
defp format_location({file, line, {module, fun, arity}}) do
mfa = Exception.format_mfa(module, fun, arity)
[format_file_line(file, line), ": ", mfa, ?\n]
end
defp format_location({file, line, nil}) do
[format_file_line(file, line), ?\n]
end
defp format_location({file, line, module}) do
[format_file_line(file, line), ": ", inspect(module), ?\n]
end
defp format_file_line(file, line) do
file = Path.relative_to_cwd(file)
line = if line > 0, do: [?: | Integer.to_string(line)], else: []
[" ", file, line]
end
defp print_warning(message) do
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
end
## Server callbacks
def init([modules, send_results, schedulers]) do
ets = :ets.new(:checker_cache, [:set, :public, {:read_concurrency, true}])
@@ -119,10 +244,6 @@ defmodule Module.ParallelChecker do
{:reply, ets, state}
end
def handle_cast(:start, %{modules: []} = state) do
{:stop, :normal, state}
end
def handle_cast(:start, state) do
{:noreply, spawn_checkers(state)}
end
@@ -176,7 +297,7 @@ defmodule Module.ParallelChecker do
send_results_pid = state.send_results
spawn_link(fn ->
warnings = Module.Checker.verify(verify, {parent, ets})
warnings = check_module(verify, {parent, ets})
send(send_results_pid, {__MODULE__, module, warnings})
send(parent, {__MODULE__, :done})
end)
@@ -209,7 +330,11 @@ defmodule Module.ParallelChecker do
end
defp cache_from_module_map(ets, map) do
exports = [{{:__info__, 1}, :def} | definitions_to_exports(map.definitions)]
exports =
[{{:__info__, 1}, :def}] ++
behaviour_exports(map) ++
definitions_to_exports(map.definitions)
deprecated = Map.new(map.deprecated)
cache_info(ets, map.module, exports, deprecated)
end
@@ -227,6 +352,7 @@ defmodule Module.ParallelChecker do
defp info_exports(module) do
Map.new(
[{{:__info__, 1}, :def}] ++
behaviour_exports(module) ++
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
Enum.map(module.__info__(:functions), &{&1, :def})
)
@@ -268,6 +394,17 @@ defmodule Module.ParallelChecker do
:ets.insert(ets, {{:cached, module}, true})
end
defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
defp behaviour_exports(%{is_behaviour: false}), do: []
defp behaviour_exports(module) when is_atom(module) do
if {:behaviour_info, 1} in module.module_info(:functions) do
[{{:behaviour_info, 1}, :def}]
else
[]
end
end
defp definitions_to_exports(definitions) do
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
if kind in [:def, :defmacro] do
+329 -189
View File
@@ -1,69 +1,75 @@
defmodule Module.Types do
@moduledoc false
defmodule Error do
defexception [:message]
end
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern}
alias Module.Types.{Expr, Pattern, Unify}
@doc """
Infer function definitions' types.
"""
def infer_definitions(file, module, defs) do
clauses = infer_signatures(file, module, defs)
infer_bodies(clauses)
end
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
stack = stack()
defp infer_signatures(file, module, defs) do
Enum.map(defs, fn {{fun, _arity} = function, kind, meta, clauses} ->
stack = head_stack()
context = head_context(file, module, function)
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
clauses =
Enum.map(clauses, fn {_meta, params, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], params})]}
stack = push_expr_stack(def_expr, stack)
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
case of_head(params, guards, stack, context) do
{:ok, _signature, context} -> {:ok, {context, body}}
{:error, reason} -> {:error, reason}
end
end)
try do
warnings_from_clause(args, guards, body, def_expr, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
{function, clauses}
error =
Error.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}
#{Macro.to_string(def_expr)}
Please report this bug: https://github.com/elixir-lang/elixir/issues
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
""")
reraise error, __STACKTRACE__
end
end)
end)
end
defp infer_bodies(clauses) do
Enum.map(clauses, fn {function, clauses} ->
errors =
Enum.flat_map(clauses, fn
{:ok, {head_context, body}} ->
stack = body_stack()
context = body_context(head_context)
defp with_file_meta(meta, file) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> meta_file
:error -> file
end
end
case Expr.of_expr(body, stack, context) do
{:ok, _type, _context} -> []
{:error, reason} -> [reason]
end
defp guards_to_expr([], left) do
left
end
{:error, reason} ->
[reason]
end)
defp guards_to_expr([guard | guards], left) do
guards_to_expr(guards, {:when, [], [left, guard]})
end
{function, errors}
end)
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
head_stack = Unify.push_expr_stack(def_expr, stack)
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
context.warnings
else
{:error, {type, error, context}} ->
[error_to_warning(type, error, context) | context.warnings]
end
end
@doc false
def of_head(params, guards, stack, context) do
with {:ok, types, context} <-
map_reduce_ok(params, context, &Pattern.of_pattern(&1, stack, &2)),
# TODO: Check that of_guard/3 returns a boolean
{:ok, _, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
do: {:ok, lift_types(types, context), context}
end
@doc false
def head_context(file, module, function) do
def context(file, module, function, no_warn_undefined, cache) do
%{
# File of module
file: file,
@@ -71,6 +77,10 @@ defmodule Module.Types do
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined
no_warn_undefined: no_warn_undefined,
# A list of cached modules received from the parallel compiler
cache: cache,
# Expression variable to type variable
vars: %{},
# Type variable to expression variable
@@ -85,18 +95,23 @@ defmodule Module.Types do
# Track if a variable was infered from a type guard function such is_tuple/1
# or a guard function that fails such as elem/2, possible values are:
# `:guarded` when `is_tuple(x)`
# `:guarded` when `is_tuple and elem(x, 0)`
# `:fail` when `elem(x, 0)`
# `:guarded_fail` when `is_tuple and elem(x, 0)`
guard_sources: %{}
guard_sources: %{},
# A list with all warnings from the running the code
warnings: []
}
end
@doc false
def head_stack() do
def stack() do
%{
# Stack of expression we have recursed through during inference,
# Stack of variables we have refined during unification,
# used for creating relevant traces
unify_stack: [],
# Last expression we have recursed through during inference,
# used for tracing
expr_stack: [],
last_expr: nil,
# When false do not add a trace when a type variable is refined,
# useful when merging contexts where the variables already have traces
trace: true,
@@ -105,47 +120,11 @@ defmodule Module.Types do
type_guards_enabled?: true,
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: :pattern
context: nil
}
end
@doc false
def body_context(head_context) do
%{
# File of module
file: head_context.file,
# Module of definitions
module: head_context.module,
# Current function
function: head_context.function,
# Expression variable to type variable
vars: head_context.vars,
# Type variable to expression variable
types_to_vars: head_context.types_to_vars,
# Type variable to type
types: head_context.types,
# Trace of all variables that have been refined to a type,
# including the type they were refined to, why, and where
traces: head_context.traces,
# Counter to give type variables unique names
counter: head_context.counter
}
end
@doc false
def body_stack() do
%{
# Stack of expression we have recursed through during inference,
# used for tracing
expr_stack: [],
# When false do not add a trace when a type variable is refined,
# useful when merging contexts where the variables already have traces
trace: true,
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: :expr
}
end
## VARIABLE LIFTING
@doc """
Lifts type variables to their infered types from the context.
@@ -161,7 +140,9 @@ defmodule Module.Types do
types
end
@doc false
@doc """
Lifts a single type to its infered type from the context.
"""
def lift_type(type, context) do
context = %{
types: context.types,
@@ -173,28 +154,6 @@ defmodule Module.Types do
type
end
## GUARDS
# TODO: Remove this and let multiple when be treated as multiple clauses,
# meaning they will be intersection types
defp guards_to_or([]) do
[]
end
defp guards_to_or(guards) do
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
end
defp guards_to_expr([], left) do
left
end
defp guards_to_expr([guard | guards], left) do
guards_to_expr(guards, {:when, [], [left, guard]})
end
## VARIABLE LIFTING
# Lift type variable to its infered (hopefully concrete) types from the context
defp do_lift_type({:var, var}, context) do
case Map.fetch(context.lifted_types, var) do
@@ -220,17 +179,17 @@ defmodule Module.Types do
end
end
defp do_lift_type({:tuple, types}, context) do
defp do_lift_type({:tuple, n, types}, context) do
{types, context} = Enum.map_reduce(types, context, &do_lift_type/2)
{{:tuple, types}, context}
{{:tuple, n, types}, context}
end
defp do_lift_type({:map, pairs}, context) do
{pairs, context} =
Enum.map_reduce(pairs, context, fn {key, value}, context ->
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
{key, context} = do_lift_type(key, context)
{value, context} = do_lift_type(value, context)
{{key, value}, context}
{{kind, key, value}, context}
end)
{{:map, pairs}, context}
@@ -254,54 +213,158 @@ defmodule Module.Types do
{type, context}
end
## ERROR FORMATTING
## ERROR TO WARNING
def format_warning({:unable_unify, left, right, expr, traces}) do
[
"function clause will never match, found incompatibility:\n\n ",
format_type(left),
" !~ ",
format_type(right),
"\n\n",
format_expr(expr),
format_traces(traces),
"Conflict found at"
]
# Collect relevant information from context and traces to report error
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
line = get_meta(stack.last_expr)[:line]
location = {context.file, line, {context.module, fun, arity}}
traces = type_traces(stack, context)
traces = tag_traces(traces, context)
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
defp format_expr(nil) do
[]
# Collect relevant traces from context.traces using stack.unify_stack
defp type_traces(stack, context) do
# TODO: Do we need the unify_stack or is enough to only get the last variable
# in the stack since we get related variables anyway?
stack =
stack.unify_stack
|> Enum.uniq()
|> Enum.flat_map(&[&1 | related_variables(&1, context.types)])
|> Enum.uniq()
Enum.flat_map(stack, fn var_index ->
with %{^var_index => traces} <- context.traces,
%{^var_index => expr_var} <- context.types_to_vars do
Enum.map(traces, &{expr_var, &1})
else
_other -> []
end
end)
end
defp format_expr(expr) do
[
"in expression:\n\n ",
expr_to_string(expr),
"\n\n"
]
defp related_variables(var, types) do
Enum.flat_map(types, fn
{related_var, {:var, ^var}} ->
[related_var | related_variables(related_var, types)]
_ ->
[]
end)
end
defp format_traces([]) do
[]
# Tag if trace is for a concrete type or type variable
defp tag_traces(traces, context) do
Enum.flat_map(traces, fn {var, {type, expr, location}} ->
with {:var, var_index} <- type,
%{^var_index => expr_var} <- context.types_to_vars do
[{var, {:var, expr_var, expr, location}}]
else
_ -> [{var, {:type, type, expr, location}}]
end
end)
end
defp format_traces(traces) do
Enum.map(traces, fn
{var, {:type, type, expr, location}} ->
## FORMAT WARNINGS
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
cond do
map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) ->
{:ok, atom, known_atoms} = missing_field(left, right)
# Drop the last trace which is the expression map.foo
traces = Enum.drop(traces, 1)
{traces, hints} = format_traces(traces, true)
[
"undefined field \"#{atom}\" ",
format_expr(expr, location),
"expected one of the following fields: ",
Enum.map_join(Enum.sort(known_atoms), ", ", & &1),
"\n\n",
traces,
format_message_hints(hints),
"Conflict found at"
]
true ->
simplify_left? = simplify_type?(left, right)
simplify_right? = simplify_type?(right, left)
{traces, hints} = format_traces(traces, simplify_left? or simplify_right?)
[
"incompatible types:\n\n ",
Unify.format_type(left, simplify_left?),
" !~ ",
Unify.format_type(right, simplify_right?),
"\n\n",
format_expr(expr, location),
traces,
format_message_hints(hints),
"Conflict found at"
]
end
end
defp missing_field(
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
{:map, fields}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(
{:map, fields},
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(_, _), do: :error
defp matched_missing_field(fields, type, atom) do
if List.keymember?(fields, type, 1) do
:error
else
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
{:ok, atom, known_atoms}
end
end
defp format_traces([], _simplify?) do
{[], []}
end
defp format_traces(traces, simplify?) do
traces
|> Enum.reverse()
|> Enum.map_reduce([], fn
{var, {:type, type, expr, location}}, hints ->
{hint, hints} = format_type_hint(type, expr, hints)
trace = [
"where \"",
Macro.to_string(var),
"\" was given the type ",
Module.Types.format_type(type),
Unify.format_type(type, simplify?),
hint,
" in:\n\n # ",
format_location(location),
" ",
expr_to_string(expr),
indent(expr_to_string(expr)),
"\n\n"
]
{var1, {:var, var2, expr, location}} ->
[
{trace, hints}
{var1, {:var, var2, expr, location}}, hints ->
trace = [
"where \"",
Macro.to_string(var1),
"\" was given the same type as \"",
@@ -309,57 +372,42 @@ defmodule Module.Types do
"\" in:\n\n # ",
format_location(location),
" ",
expr_to_string(expr),
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
end)
end
defp format_location({file, line, _mfa}) do
format_location({file, line})
end
defp format_location({file, line}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line)], else: []
[file, ?:, line, ?\n]
end
@doc false
def format_type({:union, types}) do
"#{Enum.map_join(types, " | ", &format_type/1)}"
defp simplify_type?(type, other) do
map_type?(type) and not map_type?(other)
end
def format_type({:tuple, types}) do
"{#{Enum.map_join(types, ", ", &format_type/1)}}"
## EXPRESSION FORMATTING
defp format_expr(nil, _location) do
[]
end
def format_type({:list, type}) do
"[#{format_type(type)}]"
end
def format_type({:map, pairs}) do
case List.keytake(pairs, :__struct__, 0) do
{{:__struct__, struct}, pairs} ->
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
nil ->
"%{#{format_map_pairs(pairs)}}"
end
end
def format_type({:atom, literal}) do
inspect(literal)
end
def format_type(atom) when is_atom(atom) do
"#{atom}()"
end
def format_type({:var, index}) do
"var#{index}"
end
defp format_map_pairs(pairs) do
Enum.map_join(pairs, ", ", fn {left, right} ->
"#{format_type(left)} => #{format_type(right)}"
end)
defp format_expr(expr, location) do
[
"in expression:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
end
@doc false
@@ -371,17 +419,109 @@ defmodule Module.Types do
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{:., _, [:erlang, :orelse]} -> :or
{:., _, [:erlang, :andalso]} -> :and
{{:., _, [mod, fun]}, _, args} -> erl_to_ex(mod, fun, args)
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp erl_to_ex(mod, fun, args) do
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args} -> {fun, [], args}
{mod, fun, args} -> {{:., [], [mod, fun]}, [], args}
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
## Hints
defp format_message_hints(hints) do
hints |> Enum.uniq() |> Enum.reverse() |> Enum.map(&format_message_hint/1)
end
defp format_message_hint(:inferred_dot) do
"""
HINT: "var.field" (without parentheses) implies "var" is a map() while \
"var.fun()" (with parentheses) implies "var" is an atom()
"""
end
defp format_message_hint(:inferred_bitstring_spec) do
"""
HINT: all expressions given to binaries are assumed to be of type \
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
to change the default behaviour.
"""
end
defp format_type_hint(type, expr, hints) do
case format_type_hint(type, expr) do
{message, hint} -> {message, [hint | hints]}
:error -> {[], hints}
end
end
defp format_type_hint(type, expr) do
cond do
dynamic_map_dot?(type, expr) ->
{" (due to calling var.field)", :inferred_dot}
dynamic_remote_call?(type, expr) ->
{" (due to calling var.fun())", :inferred_dot}
inferred_bitstring_spec?(type, expr) ->
{[], :inferred_bitstring_spec}
true ->
:error
end
end
defp dynamic_map_dot?(type, expr) do
with true <- map_type?(type),
{{:., _meta1, [_map, _field]}, meta2, []} <- expr,
true <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp dynamic_remote_call?(type, expr) do
with true <- atom_type?(type),
{{:., _meta1, [_module, _field]}, meta2, []} <- expr,
false <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp inferred_bitstring_spec?(type, expr) do
with true <- integer_type?(type),
{:<<>>, _, args} <- expr,
true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do
true
else
_ -> false
end
end
## Formatting helpers
defp indent(string) do
String.replace(string, "\n", " \n")
end
defp map_type?({:map, _}), do: true
defp map_type?(_other), do: false
defp atom_type?(:atom), do: true
defp atom_type?({:atom, _}), do: false
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
defp atom_type?(_other), do: false
defp integer_type?(:integer), do: true
defp integer_type?(_other), do: false
end
+492 -1
View File
@@ -1,7 +1,498 @@
defmodule Module.Types.Expr do
@moduledoc false
def of_expr(_expr, _stack, context) do
alias Module.Types.{Of, Pattern}
import Module.Types.{Helpers, Unify}
def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do
of_expr(expr, %{stack | context: :expr}, context)
end
# :atom
def of_expr(atom, _stack, context) when is_atom(atom) do
{:ok, {:atom, atom}, context}
end
# 12
def of_expr(literal, _stack, context) when is_integer(literal) do
{:ok, :integer, context}
end
# 1.2
def of_expr(literal, _stack, context) when is_float(literal) do
{:ok, :float, context}
end
# "..."
def of_expr(literal, _stack, context) when is_binary(literal) do
{:ok, :binary, context}
end
# #PID<...>
def of_expr(literal, _stack, context) when is_pid(literal) do
{:ok, :dynamic, context}
end
# <<...>>>
def of_expr({:<<>>, _meta, args}, stack, context) do
result = Of.binary(args, stack, context, &of_expr/3)
case result do
{:ok, context} -> {:ok, :binary, context}
{:error, reason} -> {:error, reason}
end
end
# left | []
def of_expr({:|, _meta, [left_expr, []]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
of_expr(left_expr, stack, context)
end
# left | right
def of_expr({:|, _meta, [left_expr, right_expr]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case of_expr(left_expr, stack, context) do
{:ok, left, context} ->
case of_expr(right_expr, stack, context) do
{:ok, {:list, right}, context} ->
{:ok, to_union([left, right], context), context}
{:ok, right, context} ->
{:ok, to_union([left, right], context), context}
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
# []
def of_expr([], _stack, context) do
{:ok, {:list, :dynamic}, context}
end
# [expr, ...]
def of_expr(exprs, stack, context) when is_list(exprs) do
stack = push_expr_stack(exprs, stack)
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
{:error, reason} -> {:error, reason}
end
end
# __CALLER__
def of_expr({:__CALLER__, _meta, var_context}, _stack, context) when is_atom(var_context) do
struct_pair = {:required, {:atom, :__struct__}, {:atom, Macro.Env}}
pairs =
Enum.map(Map.from_struct(Macro.Env.__struct__()), fn {key, _value} ->
{:required, {:atom, key}, :dynamic}
end)
{:ok, {:map, [struct_pair | pairs]}, context}
end
# __STACKTRACE__
def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context) when is_atom(var_context) do
file = {:tuple, 2, [{:atom, :file}, {:list, :integer}]}
line = {:tuple, 2, [{:atom, :line}, :integer]}
file_line = {:list, {:union, [file, line]}}
type = {:list, {:tuple, 4, [:atom, :atom, :integer, file_line]}}
{:ok, type, context}
end
# var
def of_expr(var, _stack, context) when is_var(var) do
{:ok, get_var!(var, context), context}
end
# {left, right}
def of_expr({left, right}, stack, context) do
of_expr({:{}, [], [left, right]}, stack, context)
end
# {...}
def of_expr({:{}, _meta, exprs} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
{:error, reason} -> {:error, reason}
end
end
# left = right
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, left_type, context} <-
Pattern.of_pattern(left_expr, stack, context),
{:ok, right_type, context} <- of_expr(right_expr, stack, context),
do: unify(right_type, left_type, %{stack | context: :pattern}, context)
end
# %{map | ...}
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, map_type, context} <- of_expr(map, stack, context),
{:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/3),
dynamic_value_pairs =
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
# Retrieve map type and overwrite with the new value types from the map update
{:map, pairs} = resolve_var(type, context)
updated_pairs =
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
List.keyreplace(pairs, key, 1, {:required, key, value})
end)
{:ok, {:map, updated_pairs}, context}
end
end
# %Struct{map | ...}
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, struct, context} <- Of.struct(module, meta, context),
{:ok, update, context} <- of_expr(update, stack, context) do
unify(update, struct, stack, context)
end
end
# %{...}
def of_expr({:%{}, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
Of.closed_map(args, stack, context, &of_expr/3)
end
# %Struct{...}
def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, struct, context} <- Of.struct(module, meta1, context),
{:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/3) do
unify(map, struct, stack, context)
end
end
# ()
def of_expr({:__block__, _meta, []}, _stack, context) do
{:ok, {:atom, nil}, context}
end
# (expr; expr)
def of_expr({:__block__, _meta, exprs}, stack, context) do
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context}
{:error, reason} -> {:error, reason}
end
end
# case expr do pat -> expr end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context),
{:ok, context} <- of_clauses(clauses, stack, context),
do: {:ok, :dynamic, context}
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case of_clauses(clauses, stack, context) do
{:ok, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
@try_blocks [:do, :after]
@try_clause_blocks [:catch, :else, :after]
# try do expr end
def of_expr({:try, _meta, [blocks]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{result, context} =
reduce_ok(blocks, context, fn
{:rescue, clauses}, context ->
reduce_ok(clauses, context, fn
{:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc ->
{_type, context} = new_pattern_var(var, context)
with {:ok, context} <- of_expr_context(body, stack, context) do
{:ok, keep_warnings(acc, context)}
end
{:->, _, [[var], body]}, context = acc ->
{_type, context} = new_pattern_var(var, context)
with {:ok, context} <- of_expr_context(body, stack, context) do
{:ok, keep_warnings(acc, context)}
end
end)
{block, body}, context = acc when block in @try_blocks ->
with {:ok, context} <- of_expr_context(body, stack, context) do
{:ok, keep_warnings(acc, context)}
end
{block, clauses}, context when block in @try_clause_blocks ->
of_clauses(clauses, stack, context)
end)
case result do
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# receive do pat -> expr end
def of_expr({:receive, _meta, [blocks]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{result, context} =
reduce_ok(blocks, context, fn
{:do, {:__block__, _, []}}, context ->
{:ok, context}
{:do, clauses}, context ->
of_clauses(clauses, stack, context)
{:after, [{:->, _meta, [head, body]}]}, context = acc ->
with {:ok, _type, context} <- of_expr(head, stack, context),
{:ok, _type, context} <- of_expr(body, stack, context),
do: {:ok, keep_warnings(acc, context)}
end)
case result do
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# for pat <- expr do expr end
def of_expr({:for, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)),
{:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do
if Keyword.has_key?(opts, :reduce) do
with {:ok, context} <- of_clauses(block, stack, context) do
{:ok, :dynamic, context}
end
else
with {:ok, _type, context} <- of_expr(block, stack, context) do
{:ok, :dynamic, context}
end
end
end
end
# with pat <- expr do expr end
def of_expr({:with, _meta, clauses} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do
{:ok, _context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
# fun.(arg)
def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
case of_expr(fun, stack, context) do
{:ok, _fun_type, context} ->
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
# expr.key_or_fun
def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, stack, context)
when not is_atom(expr1) do
stack = push_expr_stack(expr2, stack)
if Keyword.get(meta2, :no_parens, false) do
with {:ok, expr_type, context} <- of_expr(expr1, stack, context),
{value_var, context} = add_var(context),
pair_type = {:required, {:atom, key_or_fun}, value_var},
optional_type = {:optional, :dynamic, :dynamic},
map_field_type = {:map, [pair_type, optional_type]},
{:ok, _map_type, context} <- unify(map_field_type, expr_type, stack, context),
do: {:ok, value_var, context}
else
with {:ok, expr_type, context} <- of_expr(expr1, stack, context),
{:ok, _map_type, context} <- unify(expr_type, :atom, stack, context),
do: {:ok, :dynamic, context}
end
end
# expr.fun(arg)
def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, stack, context) do
context = Of.remote(expr1, fun, length(args), meta1, context)
stack = push_expr_stack(expr2, stack)
with {:ok, _expr_type, context} <- of_expr(expr1, stack, context),
{:ok, _fun_type, context} <- of_expr(fun, stack, context) do
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
end
# &Foo.bar/1
def of_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, _stack, context)
when is_atom(module) and is_atom(fun) do
context = Of.remote(module, fun, arity, meta, context)
{:ok, :dynamic, context}
end
# &foo/1
# & &1
def of_expr({:&, _meta, _arg}, _stack, context) do
# TODO: Function type
{:ok, :dynamic, context}
end
# fun(arg)
def of_expr({fun, _meta, args} = expr, stack, context) when is_atom(fun) and is_list(args) do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
defp for_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
# TODO: Check that of_guard/3 returns a boolean
{:ok, _guard_type, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp for_clause(list, stack, context) when is_list(list) do
reduce_ok(list, context, &for_option(&1, stack, &2))
end
defp for_clause(expr, stack, context) do
of_expr_context(expr, stack, context)
end
defp for_option({:into, expr}, stack, context) do
of_expr_context(expr, stack, context)
end
defp for_option({:reduce, expr}, stack, context) do
of_expr_context(expr, stack, context)
end
defp for_option({:uniq, _}, _stack, context) do
{:ok, context}
end
defp with_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
# TODO: Check that of_guard/3 returns a boolean
{:ok, _guard_type, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp with_clause(list, stack, context) when is_list(list) do
reduce_ok(list, context, &with_option(&1, stack, &2))
end
defp with_clause(expr, stack, context) do
of_expr_context(expr, stack, context)
end
defp with_option({:do, body}, stack, context) do
of_expr_context(body, stack, context)
end
defp with_option({:else, clauses}, stack, context) do
of_clauses(clauses, stack, context)
end
defp of_clauses(clauses, stack, context) do
reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context = acc ->
{patterns, guards} = extract_head(head)
with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context),
{:ok, _expr_type, context} <- of_expr(body, stack, context),
do: {:ok, keep_warnings(acc, context)}
end)
end
defp keep_warnings(context, %{warnings: warnings}) do
%{context | warnings: warnings}
end
defp extract_head([{:when, _meta, args}]) do
case Enum.split(args, -1) do
{patterns, [guards]} -> {patterns, flatten_when(guards)}
{patterns, []} -> {patterns, []}
end
end
defp extract_head(other) do
{other, []}
end
defp flatten_when({:when, _meta, [left, right]}) do
[left | flatten_when(right)]
end
defp flatten_when(other) do
[other]
end
defp of_expr_context(expr, stack, context) do
case of_expr(expr, stack, context) do
{:ok, _type, context} -> {:ok, context}
{:error, reason} -> {:error, reason}
end
end
defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do
{:dynamic, context}
end
defp new_pattern_var(var, context) do
new_var(var, context)
end
end
+31 -15
View File
@@ -1,4 +1,5 @@
defmodule Module.Types.Helpers do
# AST and enumeration helpers.
@moduledoc false
@doc """
@@ -19,14 +20,10 @@ defmodule Module.Types.Helpers do
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
Returns the AST metadata.
"""
def push_expr_stack(expr, stack) do
%{stack | expr_stack: [expr | stack.expr_stack]}
end
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
@@ -41,10 +38,6 @@ defmodule Module.Types.Helpers do
{:ok, acc} ->
do_reduce_ok(tail, acc, fun)
result when elem(result, 0) == :ok ->
result = Tuple.delete_at(result, 0)
do_reduce_ok(tail, result, fun)
{:error, reason} ->
{:error, reason}
end
@@ -81,10 +74,6 @@ defmodule Module.Types.Helpers do
{:ok, elem} ->
do_map_ok(tail, [elem | acc], fun)
result when elem(result, 0) == :ok ->
result = Tuple.delete_at(result, 0)
do_map_ok(tail, [result | acc], fun)
{:error, reason} ->
{:error, reason}
end
@@ -112,6 +101,23 @@ defmodule Module.Types.Helpers do
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
def flat_map_reduce_ok(list, acc, fun) do
do_flat_map_reduce_ok(list, {[], acc}, fun)
end
defp do_flat_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elems, acc} ->
do_flat_map_reduce_ok(tail, {[elems | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_reduce_ok([], {list, acc}, _fun),
do: {:ok, Enum.reverse(Enum.concat(list)), acc}
@doc """
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
@@ -123,4 +129,14 @@ defmodule Module.Types.Helpers do
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
end
end
# TODO: Remove this and let multiple when be treated as multiple clauses,
# meaning they will be intersection types
def guards_to_or([]) do
[]
end
def guards_to_or(guards) do
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
end
end
-356
View File
@@ -1,356 +0,0 @@
defmodule Module.Types.Infer do
@moduledoc false
import Module.Types.Helpers
@doc """
Unifies two types and returns the unified type and an updated typing context
or an error in case of a typing conflict.
"""
def unify(source, target, stack, context) do
case do_unify(source, target, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, reason} ->
if stack.context == :pattern do
case do_unify(target, source, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, _} ->
{:error, reason}
end
else
{:error, reason}
end
end
end
defp do_unify(same, same, _stack, context) do
{:ok, same, context}
end
defp do_unify({:var, var}, type, stack, context) do
unify_var(var, type, stack, context, _var_source = true)
end
defp do_unify(type, {:var, var}, stack, context) do
unify_var(var, type, stack, context, _var_source = false)
end
defp do_unify({:tuple, sources}, {:tuple, targets}, stack, context)
when length(sources) == length(targets) do
result =
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
unify(source, target, stack, context)
end)
case result do
{:ok, types, context} -> {:ok, {:tuple, types}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:list, source}, {:list, target}, stack, context) do
case unify(source, target, stack, context) do
{:ok, type, context} -> {:ok, {:list, type}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
# Since maps in patterns only support literal keys (excluding maps)
# we can do exact type match without subtype checking
unique_right_pairs =
Enum.reject(target_pairs, fn {key, _value} ->
:lists.keyfind(key, 1, source_pairs)
end)
unique_pairs = source_pairs ++ unique_right_pairs
# Build union of all unique key-value pairs between the maps
result =
map_reduce_ok(unique_pairs, context, fn {source_key, source_value}, context ->
case :lists.keyfind(source_key, 1, target_pairs) do
{^source_key, target_value} ->
case unify(source_value, target_value, stack, context) do
{:ok, value, context} -> {:ok, {source_key, value}, context}
{:error, reason} -> {:error, reason}
end
false ->
{:ok, {source_key, source_value}, context}
end
end)
case result do
{:ok, pairs, context} -> {:ok, {:map, pairs}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
defp do_unify(source, target, stack, context) do
if subtype?(source, target, context) do
{:ok, source, context}
else
error({:unable_unify, source, target}, stack, context)
end
end
defp unify_var(var, type, stack, context, var_source?) do
case Map.fetch!(context.types, var) do
:unbound ->
context = refine_var(var, type, stack, context)
if recursive_type?(type, [], context) do
if var_source? do
error({:unable_unify, {:var, var}, type}, stack, context)
else
error({:unable_unify, type, {:var, var}}, stack, context)
end
else
{:ok, {:var, var}, context}
end
var_type ->
context = trace_var(var, type, stack, context)
unify_result =
if var_source? do
unify(var_type, type, stack, context)
else
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, var_type, context} ->
context = refine_var(var, var_type, stack, context)
{:ok, {:var, var}, context}
{:error, reason} ->
{:error, reason}
end
end
end
@doc """
Adds a variable to the typing context and returns its type variables.
If the variable has already been added, return the existing type variable.
"""
def new_var(var, context) do
case Map.fetch(context.vars, var_name(var)) do
{:ok, type} ->
{type, context}
:error ->
type = {:var, context.counter}
vars = Map.put(context.vars, var_name(var), type)
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| vars: vars,
types_to_vars: types_to_vars,
types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
end
@doc """
Set the type for a variable and add trace.
"""
def refine_var(var, type, stack, context) do
types = Map.put(context.types, var, type)
context = %{context | types: types}
trace_var(var, type, stack, context)
end
@doc """
Remove type variable and all its traces.
"""
def remove_var(var, context) do
types = Map.delete(context.types, var)
traces = Map.delete(context.traces, var)
%{context | types: types, traces: traces}
end
defp trace_var(var, type, %{trace: true, expr_stack: expr_stack} = _stack, context) do
line = get_meta(hd(expr_stack))[:line]
trace = {type, expr_stack, {context.file, line}}
traces = Map.update!(context.traces, var, &[trace | &1])
%{context | traces: traces}
end
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
context
end
# Check if a variable is recursive and incompatible with itself
# Bad: `{var} = var`
# Good: `x = y; y = z; z = x`
defp recursive_type?({:var, var} = parent, parents, context) do
case Map.fetch!(context.types, var) do
:unbound ->
false
type ->
if type in parents do
not Enum.all?(parents, &match?({:var, _}, &1))
else
recursive_type?(type, [parent | parents], context)
end
end
end
defp recursive_type?({:list, type} = parent, parents, context) do
recursive_type?(type, [parent | parents], context)
end
defp recursive_type?({:tuple, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:map, pairs} = parent, parents, context) do
Enum.any?(pairs, fn {key, value} ->
recursive_type?(key, [parent | parents], context) or
recursive_type?(value, [parent | parents], context)
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Checks if the first argument is a subtype of the second argument.
Only checks for simple and concrete types.
"""
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
def subtype?(:boolean, :atom, _context), do: true
def subtype?(:float, :number, _context), do: true
def subtype?(:integer, :number, _context), do: true
def subtype?({:tuple, _}, :tuple, _context), do: true
# TODO: Lift unions to unify/3?
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
Enum.all?(left_types, &subtype?(&1, right_union, context))
end
def subtype?(left, {:union, right_types}, context) do
Enum.any?(right_types, &subtype?(left, &1, context))
end
def subtype?(left, right, _context), do: left == right
@doc """
Returns a "simplified" union using `subtype?/3` to remove redundant types.
Due to limitations in `subtype?/3` some overlapping types may still be
included. For example unions with overlapping non-concrete types such as
`{boolean()} | {atom()}` will not be merged or types with variables that
are distinct but equivalent such as `a | b when a ~ b`.
"""
# TODO: Translate union of all top types to dynamic()
def to_union(types, context) when types != [] do
if :dynamic in types do
:dynamic
else
case unique_super_types(flatten_union(types), context) do
[type] -> type
types -> {:union, types}
end
end
end
defp flatten_union(types) do
Enum.flat_map(types, fn
{:union, types} -> flatten_union(types)
type -> [type]
end)
end
# Filter subtypes
# `boolean() | atom()` => `atom()`
# `:foo | atom()` => `atom()`
# Does not unify `true | false` => `boolean()`
defp unique_super_types([type | types], context) do
types = Enum.reject(types, &subtype?(&1, type, context))
if Enum.any?(types, &subtype?(type, &1, context)) do
unique_super_types(types, context)
else
[type | unique_super_types(types, context)]
end
end
defp unique_super_types([], _context) do
[]
end
# Collect relevant information from context and traces to report error
defp error({:unable_unify, left, right}, stack, context) do
{fun, arity} = context.function
line = get_meta(hd(stack.expr_stack))[:line]
location = {context.file, line, {context.module, fun, arity}}
traces = type_traces(context)
common_expr = common_super_expr(traces) || hd(stack.expr_stack)
traces = simplify_traces(traces, context)
{:error, {Module.Types, {:unable_unify, left, right, common_expr, traces}, [location]}}
end
defp type_traces(context) do
Enum.flat_map(context.traces, fn {var_index, traces} ->
expr_var = Map.fetch!(context.types_to_vars, var_index)
Enum.map(traces, &{expr_var, &1})
end)
end
# Only use last expr from trace and tag if trace is for
# a concrete type or type variable
defp simplify_traces(traces, context) do
Enum.flat_map(traces, fn {var, {type, [expr | _], location}} ->
case type do
{:var, var_index} ->
var2 = Map.fetch!(context.types_to_vars, var_index)
[{var, {:var, var2, expr, location}}]
_ ->
[{var, {:type, type, expr, location}}]
end
end)
end
# Find first common super expression among all traces
defp common_super_expr([]) do
nil
end
defp common_super_expr([{_var, {_type, expr_stack, _location}} | traces]) do
Enum.find_value(expr_stack, fn expr ->
common? =
Enum.all?(traces, fn {_var, {_type, expr_stack, _location}} -> expr in expr_stack end)
if common? do
expr
end
end)
end
defp get_meta({_fun, meta, _args}) when is_list(meta), do: meta
defp get_meta(_other), do: []
end
+319
View File
@@ -0,0 +1,319 @@
defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
@prefix quote(do: ...)
@suffix quote(do: ...)
alias Module.ParallelChecker
import Module.Types.Helpers
import Module.Types.Unify
@doc """
Handles open maps (with dynamic => dynamic).
"""
def open_map(args, stack, context, fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
pairs =
for {key, value} <- pairs, not has_unbound_var?(key, context) do
if singleton?(key, context) do
{key, value}
else
{key, to_union([value, :dynamic], context)}
end
end
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
{:ok, {:map, triplets}, context}
end
end
@doc """
Handles closed maps (without dynamic => dynamic).
"""
def closed_map(args, stack, context, fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
{:ok, {:map, closed_to_unions(pairs, context)}, context}
end
end
defp map_pairs(pairs, stack, context, fun) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- fun.(key, stack, context),
{:ok, value_type, context} <- fun.(value, stack, context),
do: {:ok, {key_type, value_type}, context}
end)
end
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
defp closed_to_unions(pairs, context) do
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
{[], pairs} -> pairs_to_unions(pairs, [], context)
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
end
end
defp pairs_to_unions([{key, value} | ahead], behind, context) do
{matched_ahead, values} = find_matching_values(ahead, key, [], [])
# In case nothing matches, use the original ahead
ahead = matched_ahead || ahead
all_values =
[value | values] ++
find_subtype_values(ahead, key, context) ++
find_subtype_values(behind, key, context)
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
end
defp pairs_to_unions([], acc, context) do
acc
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|> Enum.map(fn {key, value} -> {:required, key, value} end)
end
defp find_subtype_values(pairs, key, context) do
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
end
defp find_matching_values([{key, value} | ahead], key, acc, values) do
find_matching_values(ahead, key, acc, [value | values])
end
defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do
find_matching_values(ahead, key, [pair | acc], values)
end
defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values}
defp find_matching_values([], _key, _acc, []), do: {nil, []}
@doc """
Handles structs.
"""
def struct(struct, meta, context) do
context = remote(struct, :__struct__, 0, meta, context)
entries =
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
{:required, {:atom, key}, :dynamic}
end
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
end
## Binary
@doc """
Handles binaries.
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _stack, context, _fun) do
{:ok, context}
end
def binary([head], stack, context, fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
binary_segment(head, head_stack, context, fun)
end
def binary([head | tail], stack, context, fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
case binary_segment(head, head_stack, context, fun) do
{:ok, context} -> binary_many(tail, stack, context, fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_many([last], stack, context, fun) do
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
binary_segment(last, last_stack, context, fun)
end
defp binary_many([head | tail], stack, context, fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
case binary_segment(head, head_stack, context, fun) do
{:ok, context} -> binary_many(tail, stack, context, fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, fun) do
expected_type =
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
float? = collect_binary_specifier(specifiers, &float_type?/1)
# Special case utf and float specifiers because they can be two types as literals
# but only a specific type as a variable in a pattern
cond do
stack.context == :pattern and utf? and is_binary(expr) ->
{:ok, context}
stack.context == :pattern and float? and is_integer(expr) ->
{:ok, context}
true ->
with {:ok, type, context} <- fun.(expr, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
end
defp collect_binary_specifier(other, fun) do
fun.(other)
end
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
defp binary_type(:pattern, {:float, _, _}), do: :float
defp binary_type(_context, {:integer, _, _}), do: :integer
defp binary_type(_context, {:bits, _, _}), do: :binary
defp binary_type(_context, {:bitstring, _, _}), do: :binary
defp binary_type(_context, {:bytes, _, _}), do: :binary
defp binary_type(_context, {:binary, _, _}), do: :binary
defp binary_type(_context, _specifier), do: nil
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
defp utf_type?(_), do: false
defp float_type?({:float, _, _}), do: true
defp float_type?(_), do: false
## Remote
@doc """
Handles remote calls.
"""
def remote(module, fun, arity, meta, context) when is_atom(module) do
# TODO: In the future we may want to warn for modules defined
# in the local context
if Keyword.get(meta, :context_module, false) and context.module != module do
context
else
ParallelChecker.preload_module(context.cache, module)
check_export(module, fun, arity, meta, context)
end
end
def remote(_module, _fun, _arity, _meta, context), do: context
defp check_export(module, fun, arity, meta, context) do
case ParallelChecker.fetch_export(context.cache, module, fun, arity) do
{:ok, :def, reason} ->
check_deprecated(module, fun, arity, reason, meta, context)
{:ok, :defmacro, reason} ->
context = warn(meta, context, {:unrequired_module, module, fun, arity})
check_deprecated(module, fun, arity, reason, meta, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, context) do
warn(meta, context, {:undefined_module, module, fun, arity})
else
context
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, context) do
exports = ParallelChecker.all_exports(context.cache, module)
warn(meta, context, {:undefined_function, module, fun, arity, exports})
else
context
end
end
end
defp check_deprecated(module, fun, arity, reason, meta, context) do
if reason do
warn(meta, context, {:deprecated, module, fun, arity, reason})
else
context
end
end
# The protocol code dispatches to unknown modules, so we ignore them here.
#
# try do
# SomeProtocol.Atom.__impl__
# rescue
# ...
# end
#
# But for protocols we don't want to traverse the protocol code anyway.
# TODO: remove this clause once we no longer traverse the protocol code.
defp warn_undefined?(_module, :__impl__, 1, _context), do: false
defp warn_undefined?(_module, :module_info, 0, _context), do: false
defp warn_undefined?(_module, :module_info, 1, _context), do: false
defp warn_undefined?(:erlang, :orelse, 2, _context), do: false
defp warn_undefined?(:erlang, :andalso, 2, _context), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, context) do
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
defp warn(meta, context, warning) do
{fun, arity} = context.function
location = {context.file, meta[:line] || 0, {context.module, fun, arity}}
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
end
## Warning formating
def format_warning({:undefined_module, module, fun, arity}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_warning({:undefined_function, module, fun, arity, exports}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]
end
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_warning({:unrequired_module, module, fun, arity}) do
[
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]
end
end
+131 -163
View File
@@ -1,12 +1,29 @@
defmodule Module.Types.Pattern do
@moduledoc false
import Module.Types.{Helpers, Infer}
alias Module.Types.Of
import Module.Types.{Helpers, Unify}
@doc """
Handles patterns and guards at once.
"""
def of_head(patterns, guards, stack, context) do
with {:ok, types, context} <-
map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)),
# TODO: Check that of_guard/3 returns boolean() | :fail
{:ok, _, context} <- of_guard(guards_to_or(guards), stack, context),
do: {:ok, types, context}
end
@doc """
Return the type and typing context of a pattern expression or an error
in case of a typing conflict.
"""
def of_pattern(pattern, %{context: stack_context} = stack, context)
when stack_context != :pattern do
of_pattern(pattern, %{stack | context: :pattern}, context)
end
# :atom
def of_pattern(atom, _stack, context) when is_atom(atom) do
{:ok, {:atom, atom}, context}
@@ -28,10 +45,10 @@ defmodule Module.Types.Pattern do
end
# <<...>>>
def of_pattern({:<<>>, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
def of_pattern({:<<>>, _meta, args}, stack, context) do
result = Of.binary(args, stack, context, &of_pattern/3)
case reduce_ok(args, context, &of_binary(&1, stack, &2)) do
case result do
{:ok, context} -> {:ok, :binary, context}
{:error, reason} -> {:error, reason}
end
@@ -111,6 +128,11 @@ defmodule Module.Types.Pattern do
{:ok, :dynamic, context}
end
# ^var
def of_pattern({:^, _meta, [var]}, _stack, context) do
{:ok, get_var!(var, context), context}
end
# var
def of_pattern(var, _stack, context) when is_var(var) do
{type, context} = new_var(var, context)
@@ -127,7 +149,7 @@ defmodule Module.Types.Pattern do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
{:ok, types, context} -> {:ok, {:tuple, types}, context}
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
{:error, reason} -> {:error, reason}
end
end
@@ -144,10 +166,17 @@ defmodule Module.Types.Pattern do
# %{...}
def of_pattern({:%{}, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
Of.open_map(args, stack, context, &of_pattern/3)
end
case of_pairs(args, stack, context) do
{:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context}
{:error, reason} -> {:error, reason}
# %Struct{...}
def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
when is_atom(module) do
stack = push_expr_stack(expr, stack)
with {:ok, struct, context} <- Of.struct(module, meta1, context),
{:ok, map, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
unify(map, struct, stack, context)
end
end
@@ -160,121 +189,80 @@ defmodule Module.Types.Pattern do
when is_atom(var_context) do
stack = push_expr_stack(expr, stack)
case of_pairs(args, stack, context) do
{:ok, pairs, context} ->
pairs = [{{:atom, :__struct__}, :atom} | pairs]
{:ok, {:map, pairs}, context}
{:error, reason} ->
{:error, reason}
with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
{:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context}
end
end
# %var{...}
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context)
when is_var(var) do
# %var{...} and %^var{...}
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, pairs, context} <- of_pairs(args, stack, context),
{var_type, context} = new_var(var, context),
{:ok, _, context} <- unify(var_type, :atom, stack, context) do
pairs = [{{:atom, :__struct__}, var_type} | pairs]
{:ok, {:map, pairs}, context}
with {:ok, var_type, context} = of_pattern(var, stack, context),
{:ok, _, context} <- unify(var_type, :atom, stack, context),
{:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
{:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context}
end
end
# %Struct{...}
def of_pattern({:%, _meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
when is_atom(module) do
stack = push_expr_stack(expr, stack)
case of_pairs(args, stack, context) do
{:ok, pairs, context} ->
pairs = [{{:atom, :__struct__}, {:atom, module}} | pairs]
{:ok, {:map, pairs}, context}
{:error, reason} ->
{:error, reason}
end
end
defp of_pairs(pairs, stack, context) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- of_pattern(key, stack, context),
{:ok, value_type, context} <- of_pattern(value, stack, context),
do: {:ok, {key_type, value_type}, context}
end)
end
defp pairs_to_unions(pairs, context) do
# Maps only allow simple literal keys in patterns so
# we do not have to do subtype checking
Enum.reduce(pairs, [], fn {key, value}, pairs ->
case :lists.keyfind(key, 1, pairs) do
{^key, {:union, union}} ->
:lists.keystore(key, 1, pairs, {key, to_union([value | union], context)})
{^key, original_value} ->
:lists.keystore(key, 1, pairs, {key, to_union([value, original_value], context)})
false ->
[{key, value} | pairs]
end
end)
end
def unify_kinds(:required, _), do: :required
def unify_kinds(_, :required), do: :required
def unify_kinds(:optional, :optional), do: :optional
## GUARDS
# TODO: Some guards can be changed to intersection types or higher order types
@boolean {:union, [{:atom, true}, {:atom, false}]}
@number {:union, [:integer, :float]}
@guard_functions %{
{:is_atom, 1} => {[:atom], :boolean},
{:is_binary, 1} => {[:binary], :boolean},
{:is_bitstring, 1} => {[:binary], :boolean},
{:is_boolean, 1} => {[:boolean], :boolean},
{:is_float, 1} => {[:float], :boolean},
{:is_function, 1} => {[:fun], :boolean},
{:is_function, 2} => {[:fun, :integer], :boolean},
{:is_integer, 1} => {[:integer], :boolean},
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
{:is_map, 1} => {[{:map, []}], :boolean},
{:is_map_key, 2} => {[:dynamic, {:map, []}], :dynamic},
{:is_number, 1} => {[:number], :boolean},
{:is_pid, 1} => {[:pid], :boolean},
{:is_port, 1} => {[:port], :boolean},
{:is_reference, 1} => {[:reference], :boolean},
{:is_tuple, 1} => {[:tuple], :boolean},
{:<, 2} => {[:dynamic, :dynamic], :boolean},
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
{:>, 2} => {[:dynamic, :dynamic], :boolean},
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
{:==, 2} => {[:dynamic, :dynamic], :boolean},
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
{:*, 2} => {[:number, :number], :number},
{:+, 1} => {[:number], :number},
{:+, 2} => {[:number, :number], :number},
{:-, 1} => {[:number], :number},
{:-, 2} => {[:number, :number], :number},
{:/, 2} => {[:number, :number], :number},
{:abs, 1} => {[:number], :number},
{:ceil, 1} => {[:number], :integer},
{:floor, 1} => {[:number], :integer},
{:round, 1} => {[:number], :integer},
{:trunc, 1} => {[:number], :integer},
{:is_atom, 1} => {[:atom], @boolean},
{:is_binary, 1} => {[:binary], @boolean},
{:is_bitstring, 1} => {[:binary], @boolean},
{:is_boolean, 1} => {[@boolean], @boolean},
{:is_float, 1} => {[:float], @boolean},
{:is_function, 1} => {[:fun], @boolean},
{:is_function, 2} => {[:fun, :integer], @boolean},
{:is_integer, 1} => {[:integer], @boolean},
{:is_list, 1} => {[{:list, :dynamic}], @boolean},
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean},
{:is_map_key, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
{:is_number, 1} => {[@number], @boolean},
{:is_pid, 1} => {[:pid], @boolean},
{:is_port, 1} => {[:port], @boolean},
{:is_reference, 1} => {[:reference], @boolean},
{:is_tuple, 1} => {[:tuple], @boolean},
{:<, 2} => {[:dynamic, :dynamic], @boolean},
{:"=<", 2} => {[:dynamic, :dynamic], @boolean},
{:>, 2} => {[:dynamic, :dynamic], @boolean},
{:>=, 2} => {[:dynamic, :dynamic], @boolean},
{:"/=", 2} => {[:dynamic, :dynamic], @boolean},
{:"=/=", 2} => {[:dynamic, :dynamic], @boolean},
{:==, 2} => {[:dynamic, :dynamic], @boolean},
{:"=:=", 2} => {[:dynamic, :dynamic], @boolean},
{:*, 2} => {[@number, @number], @number},
{:+, 1} => {[@number], @number},
{:+, 2} => {[@number, @number], @number},
{:-, 1} => {[@number], @number},
{:-, 2} => {[@number, @number], @number},
{:/, 2} => {[@number, @number], @number},
{:abs, 1} => {[@number], @number},
{:ceil, 1} => {[@number], :integer},
{:floor, 1} => {[@number], :integer},
{:round, 1} => {[@number], :integer},
{:trunc, 1} => {[@number], :integer},
{:element, 2} => {[:integer, :tuple], :dynamic},
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
{:length, 1} => {[{:list, :dynamic}], :integer},
{:map_get, 2} => {[:dynamic, {:map, []}], :dynamic},
{:map_size, 1} => {[{:map, []}], :integer},
{:map_get, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
{:map_size, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], :integer},
{:tl, 1} => {[{:list, :dynamic}], :dynamic},
{:tuple_size, 1} => {[:tuple], :integer},
{:node, 1} => {[{:union, [:pid, :reference, :port]}], :atom},
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
{:bit_size, 1} => {[:binary], :integer},
{:byte_size, 1} => {[:binary], :integer},
{:size, 1} => {[{:union, [:binary, :tuple]}], @boolean},
{:div, 2} => {[:integer, :integer], :integer},
{:rem, 2} => {[:integer, :integer], :integer},
{:node, 0} => {[], :atom},
@@ -285,13 +273,15 @@ defmodule Module.Types.Pattern do
{:bxor, 2} => {[:integer, :integer], :integer},
{:bsl, 2} => {[:integer, :integer], :integer},
{:bsr, 2} => {[:integer, :integer], :integer},
{:xor, 2} => {[:boolean, :boolean], :boolean},
{:not, 1} => {[:boolean], :boolean}
{:or, 2} => {[@boolean, @boolean], @boolean},
{:and, 2} => {[@boolean, @boolean], @boolean},
{:xor, 2} => {[@boolean, @boolean], @boolean},
{:not, 1} => {[@boolean], @boolean}
# Following guards are matched explicitly to handle
# type guard functions such as is_atom/1
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
# {:andalso, 2} => {[@boolean, @boolean], @boolean}
# {:orelse, 2} => {[@boolean, @boolean], @boolean}
}
@type_guards [
@@ -316,6 +306,10 @@ defmodule Module.Types.Pattern do
Refines the type variables in the typing context using type check guards
such as `is_integer/1`.
"""
def of_guard(expr, %{context: stack_context} = stack, context) when stack_context != :pattern do
of_guard(expr, %{stack | context: :pattern}, context)
end
def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
fresh_context = fresh_context(context)
@@ -323,9 +317,9 @@ defmodule Module.Types.Pattern do
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
do: {:ok, :boolean, context}
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
{:ok, _, context} <- unify(right_type, @boolean, stack, context),
do: {:ok, @boolean, context}
end
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
@@ -333,13 +327,27 @@ defmodule Module.Types.Pattern do
fresh_context = fresh_context(context)
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
do: {:ok, :boolean, context}
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
do: {:ok, @boolean, context}
end
# The unary operators + and - are special cased to avoid common warnings until
# we add support for intersection types for the guard functions
# -integer / +integer
def of_guard({{:., _, [:erlang, guard]}, _, [integer]}, _stack, context)
when guard in [:+, :-] and is_integer(integer) do
{:ok, :integer, context}
end
# -float / +float
def of_guard({{:., _, [:erlang, guard]}, _, [float]}, _stack, context)
when guard in [:+, :-] and is_float(float) do
{:ok, :float, context}
end
# fun(args)
def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{param_types, return_type} = guard_signature(guard, length(args))
@@ -353,7 +361,7 @@ defmodule Module.Types.Pattern do
with {:ok, arg_types, context} <-
map_reduce_ok(args, context, &of_guard(&1, arg_stack, &2)),
{:ok, context} <- unify_call(param_types, arg_types, stack, context) do
{:ok, context} <- unify_call(arg_types, param_types, stack, context) do
{arg_types, guard_sources} =
case arg_types do
[{:var, index} | rest_arg_types] when type_guard? ->
@@ -382,11 +390,18 @@ defmodule Module.Types.Pattern do
end
end
# map.field
def of_guard({{:., meta1, [map, field]}, meta2, []}, stack, context) do
of_guard({{:., meta1, [:erlang, :map_get]}, meta2, [field, map]}, stack, context)
end
# var
def of_guard(var, _stack, context) when is_var(var) do
type = Map.fetch!(context.vars, var_name(var))
{:ok, type, context}
end
# other literals
def of_guard(expr, stack, context) do
# Fall back to of_pattern/3 for literals
of_pattern(expr, stack, context)
@@ -398,9 +413,9 @@ defmodule Module.Types.Pattern do
%{context | types: types, traces: traces}
end
defp unify_call(params, args, stack, context) do
reduce_ok(Enum.zip(params, args), context, fn {param, arg}, context ->
case unify(param, arg, stack, context) do
defp unify_call(args, params, stack, context) do
reduce_ok(Enum.zip(args, params), context, fn {arg, param}, context ->
case unify(arg, param, stack, context) do
{:ok, _, context} -> {:ok, context}
{:error, reason} -> {:error, reason}
end
@@ -445,7 +460,6 @@ defmodule Module.Types.Pattern do
cond do
:fail in sources -> [:fail]
:guarded_fail in sources -> [:guarded_fail]
:guarded in sources -> [:guarded]
true -> []
end
@@ -456,7 +470,7 @@ defmodule Module.Types.Pattern do
# When the failing guard function wont fail due to type check function before it,
# for example: is_list(x) and length(x)
if :guarded in left and :fail in right do
[:guarded_fail]
[:guarded]
else
join_guard_source(left, right)
end
@@ -529,52 +543,6 @@ defmodule Module.Types.Pattern do
{:ok, context}
end
# binary-pattern :: specifier
defp of_binary({:"::", _meta, [expr, specifiers]} = full_expr, stack, context) do
{expected_type, utf?} = collect_binary_type(specifiers) || {:integer, false}
stack = push_expr_stack(full_expr, stack)
# Special case utf specifiers with binary literals since they allow
# both integer and binary literals but variables are always integer
if is_binary(expr) and utf? do
{:ok, context}
else
with {:ok, type, context} <- of_pattern(expr, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# binary-pattern
defp of_binary(expr, stack, context) do
case of_pattern(expr, stack, context) do
{:ok, type, context} when type in [:integer, :float, :binary] ->
{:ok, context}
{:ok, type, _context} ->
{:error, {:invalid_binary_type, type}}
{:error, reason} ->
{:error, reason}
end
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_type({:-, _meta, [left, right]}),
do: collect_binary_type(left) || collect_binary_type(right)
defp collect_binary_type({:integer, _, _}), do: {:integer, false}
defp collect_binary_type({:float, _, _}), do: {:float, false}
defp collect_binary_type({:bits, _, _}), do: {:binary, false}
defp collect_binary_type({:bitstring, _, _}), do: {:binary, false}
defp collect_binary_type({:bytes, _, _}), do: {:binary, false}
defp collect_binary_type({:binary, _, _}), do: {:binary, false}
defp collect_binary_type({:utf8, _, _}), do: {:integer, true}
defp collect_binary_type({:utf16, _, _}), do: {:integer, true}
defp collect_binary_type({:utf32, _, _}), do: {:integer, true}
defp collect_binary_type(_), do: nil
defp guard_signature(name, arity) do
Map.fetch!(@guard_functions, {name, arity})
end
+700
View File
@@ -0,0 +1,700 @@
defmodule Module.Types.Unify do
@moduledoc false
import Module.Types.Helpers
# Those are the simple types known to the system:
#
# :dynamic
# {:var, var}
# {:atom, atom} < :atom
# :integer
# :float
# :pid
# :port
# :reference
#
# Those are the composite types:
#
# {:list, type}
# {:tuple, size, [type]} < :tuple
# {:union, [type]}
# {:map, [{:required | :optional, key_type, value_type}]}
#
# Once new types are added, they should be considered in:
#
# * unify (all)
# * format_type (all)
# * subtype? (subtypes only)
# * has_unbound_var? (composite only)
# * recursive_type? (composite only)
#
@doc """
Unifies two types and returns the unified type and an updated typing context
or an error in case of a typing conflict.
"""
def unify(source, target, stack, context) do
case do_unify(source, target, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, reason} ->
if stack.context == :pattern do
case do_unify(target, source, stack, context) do
{:ok, type, context} ->
{:ok, type, context}
{:error, _} ->
{:error, reason}
end
else
{:error, reason}
end
end
end
defp do_unify(same, same, _stack, context) do
{:ok, same, context}
end
defp do_unify(type, {:var, var}, stack, context) do
case context.types do
%{^var => {:var, var_type}} ->
do_unify(type, {:var, var_type}, stack, context)
%{} ->
unify_var(var, type, stack, context, _var_source = false)
end
end
defp do_unify({:var, var}, type, stack, context) do
case context.types do
%{^var => {:var, var_type}} ->
do_unify({:var, var_type}, type, stack, context)
%{} ->
unify_var(var, type, stack, context, _var_source = true)
end
end
defp do_unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do
result =
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
unify(source, target, stack, context)
end)
case result do
{:ok, types, context} -> {:ok, {:tuple, n, types}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:list, source}, {:list, target}, stack, context) do
case unify(source, target, stack, context) do
{:ok, type, context} -> {:ok, {:list, type}, context}
{:error, reason} -> {:error, reason}
end
end
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
unify_maps(source_pairs, target_pairs, stack, context)
end
defp do_unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
defp do_unify(:dynamic, target, _stack, context) do
{:ok, target, context}
end
defp do_unify(source, target, stack, context) do
cond do
# This condition exists to handle unions with unbound vars.
# TODO: handle unions properly. Note we can easily unify
# "union < type" even if union has vars as the vars must be
# type
(match?({:union, _}, source) and has_unbound_var?(source, context)) or
(match?({:union, _}, target) and has_unbound_var?(target, context)) ->
{:ok, source, context}
subtype?(source, target, context) ->
{:ok, source, context}
true ->
error(:unable_unify, {source, target, stack}, context)
end
end
defp unify_var(var, :dynamic, _stack, context, _var_source?) do
{:ok, {:var, var}, context}
end
defp unify_var(var, type, stack, context, var_source?) do
case context.types do
%{^var => :unbound} ->
context = refine_var(var, type, stack, context)
stack = push_unify_stack(var, stack)
if recursive_type?(type, [], context) do
if var_source? do
error(:unable_unify, {{:var, var}, type, stack}, context)
else
error(:unable_unify, {type, {:var, var}, stack}, context)
end
else
{:ok, {:var, var}, context}
end
%{^var => var_type} ->
# Only add trace if the variable wasn't already "expanded"
context =
if variable_expanded?(var, stack, context) do
context
else
trace_var(var, type, stack, context)
end
stack = push_unify_stack(var, stack)
unify_result =
if var_source? do
unify(var_type, type, stack, context)
else
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, var_type, context} ->
context = refine_var(var, var_type, stack, context)
{:ok, {:var, var}, context}
{:error, reason} ->
{:error, reason}
end
end
end
# * All required keys on each side need to match to the other side.
# * All optional keys on each side that do not match must be discarded.
defp unify_maps(source_pairs, target_pairs, stack, context) do
{source_required, source_optional} = split_pairs(source_pairs)
{target_required, target_optional} = split_pairs(target_pairs)
with {:ok, source_required_pairs, context} <-
unify_source_required(source_required, target_pairs, stack, context),
{:ok, target_required_pairs, context} <-
unify_target_required(target_required, source_pairs, stack, context),
{:ok, source_optional_pairs, context} <-
unify_source_optional(source_optional, target_optional, stack, context),
{:ok, target_optional_pairs, context} <-
unify_target_optional(target_optional, source_optional, stack, context) do
# Remove duplicate pairs from matching in both left and right directions
pairs =
Enum.uniq(
source_required_pairs ++
target_required_pairs ++
source_optional_pairs ++
target_optional_pairs
)
{:ok, {:map, pairs}, context}
else
{:error, :unify} ->
error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context)
{:error, context} ->
{:error, context}
end
end
defp unify_source_required(source_required, target_pairs, stack, context) do
map_reduce_ok(source_required, context, fn {source_key, source_value}, context ->
Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{:required, source_key, source_value}]}
target_map = {:map, [{target_kind, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
defp unify_target_required(target_required, source_pairs, stack, context) do
map_reduce_ok(target_required, context, fn {target_key, target_value}, context ->
Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{source_kind, source_key, source_value}]}
target_map = {:map, [{:required, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
defp unify_source_optional(source_optional, target_optional, stack, context) do
flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context ->
Enum.find_value(target_optional, fn {target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
defp unify_target_optional(target_optional, source_optional, stack, context) do
flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context ->
Enum.find_value(source_optional, fn {source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
defp split_pairs(pairs) do
{required, optional} =
Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end)
required = Enum.map(required, fn {_kind, key, value} -> {key, value} end)
optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end)
{required, optional}
end
defp error(type, reason, context), do: {:error, {type, reason, context}}
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def push_expr_stack(expr, stack) do
%{stack | last_expr: expr}
end
@doc """
Gets a variable.
"""
def get_var!(var, context) do
Map.fetch!(context.vars, var_name(var))
end
@doc """
Adds a variable to the typing context and returns its type variable.
If the variable has already been added, return the existing type variable.
"""
def new_var(var, context) do
var_name = var_name(var)
case context.vars do
%{^var_name => type} ->
{type, context}
%{} ->
type = {:var, context.counter}
vars = Map.put(context.vars, var_name, type)
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| vars: vars,
types_to_vars: types_to_vars,
types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
end
@doc """
Adds an internal variable to the typing context and returns its type variable.
An internal variable is used to help unify complex expressions,
it does not belong to a specific AST expression.
"""
def add_var(context) do
type = {:var, context.counter}
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
@doc """
Resolves a variable raising if it is unbound.
"""
def resolve_var({:var, var}, context) do
case context.types do
%{^var => :unbound} -> raise "cannot resolve unbound var"
%{^var => type} -> resolve_var(type, context)
end
end
def resolve_var(other, _context), do: other
# Check unify stack to see if variable was already expanded
defp variable_expanded?(var, stack, context) do
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))
end
defp variable_same?(left, right, context) do
case context.types do
%{^left => {:var, new_left}} ->
variable_same?(new_left, right, context)
%{^right => {:var, new_right}} ->
variable_same?(left, new_right, context)
%{} ->
false
end
end
defp push_unify_stack(var, stack) do
%{stack | unify_stack: [var | stack.unify_stack]}
end
@doc """
Set the type for a variable and add trace.
"""
def refine_var(var, type, stack, context) do
types = Map.put(context.types, var, type)
context = %{context | types: types}
trace_var(var, type, stack, context)
end
@doc """
Remove type variable and all its traces.
"""
def remove_var(var, context) do
types = Map.delete(context.types, var)
traces = Map.delete(context.traces, var)
%{context | types: types, traces: traces}
end
defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do
line = get_meta(last_expr)[:line]
trace = {type, last_expr, {context.file, line}}
traces = Map.update!(context.traces, var, &[trace | &1])
%{context | traces: traces}
end
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
context
end
# Check if a variable is recursive and incompatible with itself
# Bad: `{var} = var`
# Good: `x = y; y = z; z = x`
defp recursive_type?({:var, var} = parent, parents, context) do
case context.types do
%{^var => :unbound} ->
false
%{^var => type} ->
if type in parents do
not Enum.all?(parents, &match?({:var, _}, &1))
else
recursive_type?(type, [parent | parents], context)
end
end
end
defp recursive_type?({:list, type} = parent, parents, context) do
recursive_type?(type, [parent | parents], context)
end
defp recursive_type?({:union, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:tuple, _, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:map, pairs} = parent, parents, context) do
Enum.any?(pairs, fn {_kind, key, value} ->
recursive_type?(key, [parent | parents], context) or
recursive_type?(value, [parent | parents], context)
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Checks if the type has a type var.
"""
def has_unbound_var?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> true
%{^var => type} -> has_unbound_var?(type, context)
end
end
def has_unbound_var?({:tuple, _, args}, context),
do: Enum.any?(args, &has_unbound_var?(&1, context))
def has_unbound_var?({:union, args}, context),
do: Enum.any?(args, &has_unbound_var?(&1, context))
def has_unbound_var?({:list, arg}, context),
do: has_unbound_var?(arg, context)
def has_unbound_var?({:map, pairs}, context) do
Enum.any?(pairs, fn {_, key, value} ->
has_unbound_var?(key, context) or has_unbound_var?(value, context)
end)
end
def has_unbound_var?(_type, _context), do: false
@doc """
Returns true if it is a singleton type.
Only atoms are singleton types. Unbound vars are not
considered singleton types.
"""
def singleton?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> singleton?(type, context)
end
end
def singleton?({:atom, _}, _context), do: true
def singleton?(_type, _context), do: false
@doc """
Checks if the first argument is a subtype of the second argument.
This function assumes that:
* unbound variables are not subtype of anything
* dynamic is not considered a subtype of all other types but the top type.
This allows this function can be used for ordering, in other cases, you
may need to check for both sides
"""
def subtype?(type, type, _context), do: true
def subtype?({:var, var}, other, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(type, other, context)
end
end
def subtype?(other, {:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(other, type, context)
end
end
def subtype?(_, :dynamic, _context), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
# Composite
def subtype?({:tuple, _, _}, :tuple, _context), do: true
def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do
left_types
|> Enum.zip(right_types)
|> Enum.any?(fn {left, right} -> subtype?(left, right, context) end)
end
def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do
Enum.all?(left_pairs, fn
{:required, left_key, left_value} ->
Enum.any?(right_pairs, fn {_, right_key, right_value} ->
subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context)
end)
{:optional, _, _} ->
true
end)
end
def subtype?({:list, left}, {:list, right}, context) do
subtype?(left, right, context)
end
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
Enum.all?(left_types, &subtype?(&1, right_union, context))
end
def subtype?(left, {:union, right_types}, context) do
Enum.any?(right_types, &subtype?(left, &1, context))
end
def subtype?({:union, left_types}, right, context) do
Enum.all?(left_types, &subtype?(&1, right, context))
end
def subtype?(_left, _right, _context), do: false
@doc """
Returns a "simplified" union using `subtype?/3` to remove redundant types.
Due to limitations in `subtype?/3` some overlapping types may still be
included. For example unions with overlapping non-concrete types such as
`{boolean()} | {atom()}` will not be merged or types with variables that
are distinct but equivalent such as `a | b when a ~ b`.
"""
def to_union([type], _context), do: type
def to_union(types, context) when types != [] do
flat_types = flatten_union(types)
case unique_super_types(flat_types, context) do
[type] -> type
types -> {:union, types}
end
end
defp flatten_union(types) do
Enum.flat_map(types, fn
{:union, types} -> flatten_union(types)
type -> [type]
end)
end
# Filter subtypes
#
# `boolean() | atom()` => `atom()`
# `:foo | atom()` => `atom()`
#
# Does not merge `true | false` => `boolean()`
defp unique_super_types([type | types], context) do
types = Enum.reject(types, &subtype?(&1, type, context))
if Enum.any?(types, &subtype?(type, &1, context)) do
unique_super_types(types, context)
else
[type | unique_super_types(types, context)]
end
end
defp unique_super_types([], _context) do
[]
end
@doc """
Formats types.
The second argument says when complex types such as maps and
structs should be simplified and not shown.
"""
def format_type({:map, pairs}, true) do
case List.keyfind(pairs, {:atom, :__struct__}, 1) do
{:required, {:atom, :__struct__}, {:atom, struct}} ->
"%#{inspect(struct)}{}"
_ ->
"map()"
end
end
def format_type({:union, types}, simplify?) do
"#{Enum.map_join(types, " | ", &format_type(&1, simplify?))}"
end
def format_type({:tuple, _, types}, simplify?) do
"{#{Enum.map_join(types, ", ", &format_type(&1, simplify?))}}"
end
def format_type({:list, type}, simplify?) do
"[#{format_type(type, simplify?)}]"
end
def format_type({:map, pairs}, false) do
case List.keytake(pairs, {:atom, :__struct__}, 1) do
{{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} ->
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
_ ->
"%{#{format_map_pairs(pairs)}}"
end
end
def format_type({:atom, literal}, _simplify?) do
inspect(literal)
end
def format_type({:var, index}, _simplify?) do
"var#{index}"
end
def format_type(atom, _simplify?) when is_atom(atom) do
"#{atom}()"
end
defp format_map_pairs(pairs) do
{atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1))
{required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1))
Enum.map_join(atoms ++ required ++ optional, ", ", fn
{:required, {:atom, atom}, right} ->
"#{atom}: #{format_type(right, false)}"
{:required, left, right} ->
"#{format_type(left, false)} => #{format_type(right, false)}"
{:optional, left, right} ->
"optional(#{format_type(left, false)}) => #{format_type(right, false)}"
end)
end
end
+3 -3
View File
@@ -106,7 +106,7 @@ defmodule Node do
For more information, see `:erlang.monitor_node/2`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
"""
@spec monitor(t, boolean) :: true
def monitor(node, flag) do
@@ -119,7 +119,7 @@ defmodule Node do
For more information, see `:erlang.monitor_node/3`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
"""
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
def monitor(node, flag, options) do
@@ -219,7 +219,7 @@ defmodule Node do
If `node` does not exist, a useless PID is returned.
For the list of available options, see `:erlang.spawn/5`.
For the list of available options, see `:erlang.spawn/4`.
Inlined by the compiler.
"""
+2 -5
View File
@@ -152,8 +152,8 @@ defmodule Path do
## Examples
Path.expand("/foo/bar/../bar")
#=> "/foo/bar"
Path.expand("/foo/bar/../baz")
#=> "/foo/baz"
"""
@spec expand(t) :: binary
@@ -565,9 +565,6 @@ defmodule Path do
"""
@spec split(t) :: [binary]
# Work around a bug in Erlang on Unix-like operating systems
def split(""), do: []
def split(path) do
:filename.split(IO.chardata_to_string(path))
end
+4 -4
View File
@@ -29,7 +29,7 @@ defmodule Port do
After sending those two messages, we invoked the IEx helper `flush()`,
which printed all messages received from the port, in this case we got
"hello" and "world" back. Notice the messages are in binary because we
"hello" and "world" back. Note that the messages are in binary because we
passed the `:binary` option when opening the port in `Port.open/2`. Without
such option, it would have yielded a list of bytes.
@@ -132,9 +132,9 @@ defmodule Port do
While we encourage graceful termination by detecting if stdin/stdout has been
closed, we do not always have control over how third-party software terminates.
In those cases, you can wrap the application in a script that checks for stdin.
Here is such script in `sh`:
Here is such script that has been verified to work on bash shells:
#!/bin/sh
#!/usr/bin/env bash
# Start the program in the background
exec "$@" &
@@ -180,7 +180,7 @@ defmodule Port do
@type name ::
{:spawn, charlist | binary}
| {:spawn_driver, charlist | binary}
| {:spawn_executable, charlist | atom}
| {:spawn_executable, :file.name_all()}
| {:fd, non_neg_integer, non_neg_integer}
@doc """
+2 -1
View File
@@ -705,7 +705,8 @@ defmodule Process do
"""
@spec flag(:error_handler, module) :: module
@spec flag(:max_heap_size, heap_size) :: heap_size
@spec flag(:message_queue_data, :erlang.message_queue_data()) :: :erlang.message_queue_data()
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:priority, priority_level) :: priority_level
+5 -1
View File
@@ -43,10 +43,14 @@ defmodule Protocol do
def size(tuple), do: tuple_size(tuple)
end
Notice we didn't implement it for lists as we don't have the
Note that we didn't implement it for lists as we don't have the
`size` information on lists, rather its value needs to be
computed with `length`.
The data structure you are implementing the protocol for
must be the first argument to all functions defined in the
protocol.
It is possible to implement protocols for all Elixir types:
* Structs (see below)
+31 -26
View File
@@ -401,6 +401,7 @@ defmodule Record do
# Using {} here is safe, since it's not valid AST
default = if Macro.Env.in_match?(caller), do: {:_, [], nil}, else: {}
{default, keyword} = Keyword.pop(keyword, :_, default)
{keyword, exprs} = hoist_expressions(keyword, caller)
{elements, remaining} =
Enum.map_reduce(fields, keyword, fn {key, field_default}, remaining ->
@@ -413,6 +414,7 @@ defmodule Record do
case remaining do
[] ->
quote(do: {unquote(tag), unquote_splicing(elements)})
|> maybe_prepend_reversed_exprs(exprs)
[{key, _} | _] ->
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
@@ -425,27 +427,45 @@ defmodule Record do
raise ArgumentError, "cannot invoke update style macro inside match"
end
{keyword, exprs} = hoist_expressions(keyword, caller)
if Keyword.has_key?(keyword, :_) do
message = "updating a record with a default (:_) is equivalent to creating a new record"
IO.warn(message, Macro.Env.stacktrace(caller))
create(tag, fields, keyword, caller)
else
case build_updates(keyword, fields, [], [], []) do
{updates, [], []} ->
build_update(updates, var)
{updates, vars, exprs} ->
quote do
{unquote_splicing(:lists.reverse(vars))} = {unquote_splicing(:lists.reverse(exprs))}
unquote(build_update(updates, var))
updates =
Enum.map(keyword, fn {key, value} ->
if index = find_index(fields, key, 2) do
{index, value}
else
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
end
end)
{:error, key} ->
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
end
build_update(updates, var) |> maybe_prepend_reversed_exprs(exprs)
end
end
defp hoist_expressions(keyword, %{context: nil}) do
Enum.map_reduce(keyword, [], fn {key, expr}, acc ->
if simple_argument?(expr) do
{{key, expr}, acc}
else
var = Macro.var(key, __MODULE__)
{{key, var}, [{:=, [], [var, expr]} | acc]}
end
end)
end
defp hoist_expressions(keyword, _), do: {keyword, []}
defp maybe_prepend_reversed_exprs(expr, []),
do: expr
defp maybe_prepend_reversed_exprs(expr, exprs),
do: {:__block__, [], :lists.reverse([expr | exprs])}
defp build_update(updates, initial) do
updates
|> Enum.sort(fn {left, _}, {right, _} -> right <= left end)
@@ -454,21 +474,6 @@ defmodule Record do
end)
end
defp build_updates([{key, value} | rest], fields, updates, vars, exprs) do
if index = find_index(fields, key, 2) do
if simple_argument?(value) do
build_updates(rest, fields, [{index, value} | updates], vars, exprs)
else
var = Macro.var(key, __MODULE__)
build_updates(rest, fields, [{index, var} | updates], [var | vars], [value | exprs])
end
else
{:error, key}
end
end
defp build_updates([], _fields, updates, vars, exprs), do: {updates, vars, exprs}
defp simple_argument?({name, _, ctx}) when is_atom(name) and is_atom(ctx), do: true
defp simple_argument?(other), do: Macro.quoted_literal?(other)
+18 -20
View File
@@ -16,13 +16,14 @@ defmodule Regex do
~r/foo/iu
Regular expressions created via sigils are pre-compiled and stored
in the `.beam` file. Notice this may be a problem if you are precompiling
in the `.beam` file. Note that this may be a problem if you are precompiling
Elixir, see the "Precompilation" section for more information.
A Regex is represented internally as the `Regex` struct. Therefore,
`%Regex{}` can be used whenever there is a need to match on them.
Keep in mind it is not guaranteed two regular expressions from the
same source are equal, for example:
Keep in mind that all of the structs fields are private. There is
also not guarantee two regular expressions from the same source are
equal, for example:
~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/
@@ -37,9 +38,10 @@ defmodule Regex do
The modifiers available when creating a Regex are:
* `unicode` (u) - enables Unicode specific patterns like `\p` and change
modifiers like `\w`, `\W`, `\s` and friends to also match on Unicode.
It expects valid Unicode strings to be given on match
* `unicode` (u) - enables Unicode specific patterns like `\p` and causes
character classes like `\w`, `\W`, `\s`, etc. to also match on Unicode
(see examples below in "Character classes"). It expects valid Unicode
strings to be given on match
* `caseless` (i) - adds case insensitivity
@@ -125,6 +127,10 @@ defmodule Regex do
false
iex> String.match?("josé", ~r/^[[:lower:]]+$/u)
true
iex> Regex.replace(~r/\s/, "Unicode\u00A0spaces", "-")
"Unicode spaces"
iex> Regex.replace(~r/\s/u, "Unicode\u00A0spaces", "-")
"Unicode-spaces"
## Precompilation
@@ -218,7 +224,7 @@ defmodule Regex do
and recompiles the regex in case of version mismatch.
"""
@doc since: "1.4.0"
@spec recompile(t) :: t
@spec recompile(t) :: {:ok, t} | {:error, any}
def recompile(%Regex{} = regex) do
version = version()
@@ -273,17 +279,9 @@ defmodule Regex do
@doc """
Returns `true` if the given `term` is a regex.
Otherwise returns `false`.
## Examples
iex> Regex.regex?(~r/foo/)
true
iex> Regex.regex?(0)
false
"""
@spec regex?(any) :: boolean
# TODO: deprecate permanently on Elixir v1.15
@doc deprecated: "Use Kernel.is_struct/2 or pattern match on %Regex{} instead"
def regex?(term)
def regex?(%Regex{}), do: true
def regex?(_), do: false
@@ -759,12 +757,12 @@ defmodule Regex do
end
end
defp get_index(_string, {pos, _len}) when pos < 0 do
defp get_index(_string, {pos, _length}) when pos < 0 do
""
end
defp get_index(string, {pos, len}) do
<<_::size(pos)-binary, res::size(len)-binary, _::binary>> = string
defp get_index(string, {pos, length}) do
<<_::size(pos)-binary, res::size(length)-binary, _::binary>> = string
res
end
+36 -8
View File
@@ -3,7 +3,7 @@ defmodule Registry do
A local, decentralized and scalable key-value process storage.
It allows developers to lookup one or more processes with a given key.
If the registry has `:unique` keys, a key points to 0 or 1 processes.
If the registry has `:unique` keys, a key points to 0 or 1 process.
If the registry allows `:duplicate` keys, a single key may point to any
number of processes. In both cases, different keys could identify the
same process.
@@ -204,7 +204,7 @@ defmodule Registry do
@type guards :: [guard]
@typedoc "A pattern used to representing the output format part of a match spec"
@type body :: [atom | tuple]
@type body :: [term]
@typedoc "A full match spec used when selecting objects in the registry"
@type spec :: [{match_pattern, guards, body}]
@@ -261,9 +261,8 @@ defmodule Registry do
end
@doc false
def unregister_name({registry, key}) do
unregister(registry, key)
end
def unregister_name({registry, key}), do: unregister(registry, key)
def unregister_name({registry, key, _value}), do: unregister(registry, key)
## Registry API
@@ -784,7 +783,8 @@ defmodule Registry do
end
@doc """
Unregister entries for a given key matching a pattern.
Unregisters entries for keys matching a pattern associated to the current
process in `registry`.
## Examples
@@ -922,7 +922,7 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key_partition)
{pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition)
# Notice we write first to the pid_ets table because it will
# Note that we write first to the pid_ets table because it will
# always be able to do the cleanup. If we register first to the
# key one and the process crashes, the key will stay there forever.
Process.link(pid_server)
@@ -958,7 +958,7 @@ defmodule Registry do
if :ets.insert_new(key_ets, entry) do
:ok
else
# Notice we have to call register_key recursively
# Note that we have to call register_key recursively
# because we are always at odds of a race condition.
case :ets.lookup(key_ets, key) do
[{^key, {pid, _}} = current] ->
@@ -1033,6 +1033,34 @@ defmodule Registry do
end
end
@doc """
Deletes registry metadata for the given `key` in `registry`.
## Examples
iex> Registry.start_link(keys: :unique, name: Registry.DeleteMetaTest)
iex> Registry.put_meta(Registry.DeleteMetaTest, :custom_key, "custom_value")
:ok
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.delete_meta(Registry.DeleteMetaTest, :custom_key)
:ok
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
:error
"""
@doc since: "1.11.0"
@spec delete_meta(registry, meta_key) :: :ok
def delete_meta(registry, key) when is_atom(registry) and (is_atom(key) or is_tuple(key)) do
try do
:ets.delete(registry, key)
:ok
catch
:error, :badarg ->
raise ArgumentError, "unknown registry: #{inspect(registry)}"
end
end
@doc """
Returns the number of registered keys in a registry.
It runs in constant time.
+12 -7
View File
@@ -21,7 +21,7 @@ defmodule Stream do
iex> Enum.map(stream, &(&1 + 1))
[3, 5, 7]
Notice we started with a range and then we created a stream that is
Note that we started with a range and then we created a stream that is
meant to multiply each element in the range by 2. At this point, no
computation was done. Only when `Enum.map/2` is called we actually
enumerate over each element in the range, multiplying it by 2 and adding 1.
@@ -46,7 +46,7 @@ defmodule Stream do
6
#=> [2, 4, 6]
Notice that we first printed each element in the list, then multiplied each
Note that we first printed each element in the list, then multiplied each
element by 2 and finally printed each new value. In this example, the list
was enumerated three times. Let's see an example with streams:
@@ -68,7 +68,7 @@ defmodule Stream do
its double. In this example, the list was enumerated just once!
That's what we meant when we said earlier that streams are composable,
lazy enumerables. Notice we could call `Stream.map/2` multiple times,
lazy enumerables. Note that we could call `Stream.map/2` multiple times,
effectively composing the streams and keeping them lazy. The computations
are only performed when you call a function from the `Enum` module.
@@ -107,6 +107,7 @@ defmodule Stream do
@type index :: non_neg_integer
@type default :: any
@type timer :: non_neg_integer | :infinity
# Require Stream.Reducers and its callbacks
require Stream.Reducers, as: R
@@ -505,8 +506,10 @@ defmodule Stream do
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
@spec interval(non_neg_integer) :: Enumerable.t()
def interval(n) when is_integer(n) and n >= 0 do
@spec interval(timer()) :: Enumerable.t()
def interval(n)
when is_integer(n) and n >= 0
when n == :infinity do
unfold(0, fn count ->
Process.sleep(n)
{count, count + 1}
@@ -787,8 +790,10 @@ defmodule Stream do
[0]
"""
@spec timer(non_neg_integer) :: Enumerable.t()
def timer(n) when is_integer(n) and n >= 0 do
@spec timer(timer()) :: Enumerable.t()
def timer(n)
when is_integer(n) and n >= 0
when n == :infinity do
take(interval(n), 1)
end
+125 -48
View File
@@ -60,7 +60,7 @@ defmodule String do
Note it is generally not advised to use `\xNN` in Elixir
strings, as introducing an invalid byte sequence would
make the string invalid. If you have to introduce a
character by its hexdecimal representation, it is best
character by its hexadecimal representation, it is best
to work with Unicode code points, such as `\uNNNN`. In fact,
understanding Unicode code points can be essential when doing
low-level manipulations of string, so let's explore them in
@@ -193,14 +193,15 @@ defmodule String do
## Integer code points
Although code points could be represented as integers, this
module represents all code points as strings. For example:
Although code points are represented as integers, this module
represents code points in their encoded format as strings.
For example:
iex> String.codepoints("olá")
["o", "l", "á"]
There are a couple of ways to retrieve a character integer
code point. One may use the `?` construct:
There are a couple of ways to retrieve the character code point.
One may use the `?` construct:
iex> ?o
111
@@ -220,9 +221,9 @@ defmodule String do
iex> "ol\u00E1"
"olá"
Finally, to convert a String into a list of integers
code points, usually known as "char lists", you can call
`Strig.to_charlist`:
Finally, to convert a String into a list of integer
code points, known as "charlists" in Elixir, you can call
`String.to_charlist`:
iex> String.to_charlist("olá")
[111, 108, 225]
@@ -247,7 +248,7 @@ defmodule String do
## Compile binary patterns
Many functions in this module work with patterns. For example,
`String.split/2` can split a string into multiple strings given
`String.split/3` can split a string into multiple strings given
a pattern. This pattern can be a string, a list of strings or
a compiled pattern:
@@ -282,7 +283,7 @@ defmodule String do
@typedoc "Multiple code points that may be perceived as a single character by readers"
@type grapheme :: t
@typedoc "Pattern used in functions like `replace/3` and `split/2`"
@typedoc "Pattern used in functions like `replace/4` and `split/3`"
@type pattern :: t | [t] | :binary.cp()
@conditional_mappings [:greek]
@@ -373,9 +374,10 @@ defmodule String do
@doc ~S"""
Divides a string into parts based on a pattern.
Returns a list of these parts. The pattern can
be a string, a list of strings, a regular expression,
or a compiled pattern.
Returns a list of these parts.
The `pattern` may be a string, a list of strings, a regular expression, or a
compiled pattern.
The string is split into as many parts as possible by
default, but can be controlled via the `:parts` option.
@@ -455,13 +457,13 @@ defmodule String do
For example, take the grapheme "é" which is made of the characters
"e" and the acute accent. The following will split the string into two parts:
iex> String.split(:unicode.characters_to_nfd_binary("é"), "e")
iex> String.split(String.normalize("é", :nfd), "e")
["", "́"]
However, if "é" is represented by the single character "e with acute"
accent, then it will split the string into just one part:
iex> String.split(:unicode.characters_to_nfc_binary("é"), "e")
iex> String.split(String.normalize("é", :nfc), "e")
["é"]
"""
@@ -650,9 +652,9 @@ defmodule String do
String.normalize(string1, :nfd) == String.normalize(string2, :nfd)
Therefore, if you plan to compare multiple strings, multiple times
in a row, you may normalize them upfront and compare them directly
to avoid multiple normalization passes.
If you plan to compare multiple strings, multiple times in a row, you
may normalize them upfront and compare them directly to avoid multiple
normalization passes.
## Examples
@@ -674,21 +676,83 @@ defmodule String do
normalize(string1, :nfd) == normalize(string2, :nfd)
end
@doc false
@deprecated "Use :unicode.characters_to_nfc_binary/1 or :unicode.characters_to_nfd_binary/1 instead"
@doc """
Converts all characters in `string` to Unicode normalization
form identified by `form`.
Invalid Unicode codepoints are skipped and the remaining of
the string is converted. If you want the algorithm to stop
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`,
`:unicode.characters_to_nfc_binary/1`, `:unicode.characters_to_nfkd_binary/1`,
and `:unicode.characters_to_nfkc_binary/1` instead.
Normalization forms `:nfkc` and `:nfkd` should not be blindly applied
to arbitrary text. Because they erase many formatting distinctions,
they will prevent round-trip conversion to and from many legacy
character sets.
## Forms
The supported forms are:
* `:nfd` - Normalization Form Canonical Decomposition.
Characters are decomposed by canonical equivalence, and
multiple combining characters are arranged in a specific
order.
* `:nfc` - Normalization Form Canonical Composition.
Characters are decomposed and then recomposed by canonical equivalence.
* `:nfkd` - Normalization Form Compatibility Decomposition.
Characters are decomposed by compatibility equivalence, and
multiple combining characters are arranged in a specific
order.
* `:nfkc` - Normalization Form Compatibility Composition.
Characters are decomposed and then recomposed by compatibility equivalence.
## Examples
iex> String.normalize("yêṩ", :nfd)
"yêṩ"
iex> String.normalize("leña", :nfc)
"leña"
iex> String.normalize("fi", :nfkd)
"fi"
iex> String.normalize("fi", :nfkc)
"fi"
"""
def normalize(string, form)
def normalize(string, :nfd) do
case :unicode.characters_to_nfd_binary(string) do
string when is_binary(string) -> string
{:error, bad, rest} -> bad <> normalize(rest, :nfd)
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfd)
end
end
def normalize(string, :nfc) do
case :unicode.characters_to_nfc_binary(string) do
string when is_binary(string) -> string
{:error, bad, rest} -> bad <> normalize(rest, :nfc)
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfc)
end
end
def normalize(string, :nfkd) do
case :unicode.characters_to_nfkd_binary(string) do
string when is_binary(string) -> string
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkd)
end
end
def normalize(string, :nfkc) do
case :unicode.characters_to_nfkc_binary(string) do
string when is_binary(string) -> string
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkc)
end
end
@@ -1252,12 +1316,12 @@ defmodule String do
end
defp pad(kind, string, count, padding) do
string_len = length(string)
string_length = length(string)
if string_len >= count do
if string_length >= count do
string
else
filler = build_filler(count - string_len, padding, padding, 0, [])
filler = build_filler(count - string_length, padding, padding, 0, [])
case kind do
:leading -> [filler | string]
@@ -1293,26 +1357,26 @@ defmodule String do
@doc false
@deprecated "Use String.pad_leading/2 instead"
def rjust(subject, len) do
rjust(subject, len, ?\s)
def rjust(subject, length) do
rjust(subject, length, ?\s)
end
@doc false
@deprecated "Use String.pad_leading/3 with a binary padding instead"
def rjust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
pad(:leading, subject, len, [<<pad::utf8>>])
def rjust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
pad(:leading, subject, length, [<<pad::utf8>>])
end
@doc false
@deprecated "Use String.pad_trailing/2 instead"
def ljust(subject, len) do
ljust(subject, len, ?\s)
def ljust(subject, length) do
ljust(subject, length, ?\s)
end
@doc false
@deprecated "Use String.pad_trailing/3 with a binary padding instead"
def ljust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
pad(:trailing, subject, len, [<<pad::utf8>>])
def ljust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
pad(:trailing, subject, length, [<<pad::utf8>>])
end
@doc ~S"""
@@ -1321,7 +1385,8 @@ defmodule String do
The `subject` is always a string.
The `pattern` may be a string, a regular expression, or a compiled pattern.
The `pattern` may be a string, a list of strings, a regular expression, or a
compiled pattern.
The `replacement` may be a string or a function that receives the matched
pattern and must return the replacement as a string or iodata.
@@ -1356,7 +1421,7 @@ defmodule String do
iex> String.replace("a,b,c", ~r/,(.)/, ",\\1\\g{1}")
"a,bb,cc"
Notice we had to escape the backslash escape character (i.e., we used `\\N`
Note that we had to escape the backslash escape character (i.e., we used `\\N`
instead of just `\N` to escape the backslash; same thing for `\\g{N}`). By
giving `\0`, one can inject the whole match in the replacement string.
@@ -1531,10 +1596,13 @@ defmodule String do
:binary.copy(subject, n)
end
@doc """
Returns all code points in the string.
@doc ~S"""
Returns a list of code points encoded as strings.
For details about code points and graphemes, see the `String` module documentation.
To retrieve code points in their natural integer
representation, see `to_charlist/1`. For details about
code points and graphemes, see the `String` module
documentation.
## Examples
@@ -1736,6 +1804,9 @@ defmodule String do
iex> String.next_grapheme("olá")
{"o", "lá"}
iex> String.next_grapheme("")
nil
"""
@spec next_grapheme(t) :: {grapheme, t} | nil
def next_grapheme(binary) do
@@ -1746,9 +1817,9 @@ defmodule String do
end
@doc """
Returns the size of the next grapheme.
Returns the size (in bytes) of the next grapheme.
The result is a tuple with the next grapheme size and
The result is a tuple with the next grapheme size in bytes and
the remainder of the string or `nil` in case the string
reached its end.
@@ -1757,6 +1828,9 @@ defmodule String do
iex> String.next_grapheme_size("olá")
{1, "lá"}
iex> String.next_grapheme_size("")
nil
"""
@spec next_grapheme_size(t) :: {pos_integer, t} | nil
defdelegate next_grapheme_size(string), to: String.Unicode
@@ -1773,6 +1847,9 @@ defmodule String do
iex> String.first("եոգլի")
"ե"
iex> String.first("")
nil
"""
@spec first(t) :: grapheme | nil
def first(string) do
@@ -1866,8 +1943,7 @@ defmodule String do
end
@doc """
Returns a substring starting at the offset `start`, and of
length `len`.
Returns a substring starting at the offset `start`, and of the given `length`.
If the offset is greater than string length, then it returns `""`.
@@ -1908,22 +1984,22 @@ defmodule String do
""
end
def slice(string, start, len) when start >= 0 and len >= 0 do
def slice(string, start, length) when start >= 0 and length >= 0 do
case String.Unicode.split_at(string, start) do
{_, nil} ->
""
{start_bytes, rest} ->
{len_bytes, _} = String.Unicode.split_at(rest, len)
{len_bytes, _} = String.Unicode.split_at(rest, length)
binary_part(string, start_bytes, len_bytes)
end
end
def slice(string, start, len) when start < 0 and len >= 0 do
def slice(string, start, length) when start < 0 and length >= 0 do
start = length(string) + start
case start >= 0 do
true -> slice(string, start, len)
true -> slice(string, start, length)
false -> ""
end
end
@@ -2174,13 +2250,13 @@ defmodule String do
For example, take the grapheme "é" which is made of the characters
"e" and the acute accent. The following returns `true`:
iex> String.contains?(:unicode.characters_to_nfd_binary("é"), "e")
iex> String.contains?(String.normalize("é", :nfd), "e")
true
However, if "é" is represented by the single character "e with acute"
accent, then it will return `false`:
iex> String.contains?(:unicode.characters_to_nfc_binary("é"), "e")
iex> String.contains?(String.normalize("é", :nfc), "e")
false
"""
@@ -2527,6 +2603,7 @@ defmodule String do
[eq: "fox ", del: "ho", ins: "jum", eq: "ps over the ", del: "dog", ins: "lazy cat"]
"""
@doc since: "1.3.0"
@spec myers_difference(t, t) :: [{:eq | :ins | :del, t}]
def myers_difference(string1, string2) do
graphemes(string1)
+14 -13
View File
@@ -68,7 +68,7 @@ defmodule Supervisor do
Supervisor.count_children(pid)
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
Notice that when starting the GenServer, we are registering it
Note that when starting the GenServer, we are registering it
with name `Stack`, which allows us to call it directly and get
what is on the stack:
@@ -108,7 +108,7 @@ defmodule Supervisor do
The child specification describes how the supervisor starts, shuts down,
and restarts child processes.
The child specification is a map which contains 6 elements. The first two keys
The child specification is a map containing up to 6 elements. The first two keys
in the following list are required, and the remaining ones are optional:
* `:id` - any term used to identify the child specification
@@ -194,7 +194,7 @@ defmodule Supervisor do
start: {Stack, :start_link, [[:hello]]}
}
The map above defines a supervisor with `:id` of `Stack` that is started
The map above defines a child with `:id` of `Stack` that is started
by calling `Stack.start_link([:hello])`.
However, specifying the child specification for each child as a map can be
@@ -433,7 +433,7 @@ defmodule Supervisor do
restarts in transient mode, and linked processes exit with the same
reason unless they're trapping exits
Notice that the supervisor that reaches maximum restart intensity will exit with
Note that the supervisor that reaches maximum restart intensity will exit with
`:shutdown` reason. In this case the supervisor will only be restarted if its
child specification was defined with the `:restart` option set to `:permanent`
(the default).
@@ -823,10 +823,11 @@ defmodule Supervisor do
end
def start_child(supervisor, args) when is_list(args) do
# TODO: Deprecate in v1.11
# IO.warn(
# "Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead"
# )
IO.warn_once(
{__MODULE__, :start_child},
"Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead",
_stacktrace_drop_levels = 2
)
call(supervisor, {:start_child, args})
end
@@ -853,10 +854,9 @@ defmodule Supervisor do
def terminate_child(supervisor, child_id)
def terminate_child(supervisor, pid) when is_pid(pid) do
# TODO: Deprecate in v1.11
# IO.warn(
# "Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
# )
IO.warn(
"Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
)
call(supervisor, {:terminate_child, pid})
end
@@ -929,7 +929,8 @@ defmodule Supervisor do
"""
@spec which_children(supervisor) :: [
{term() | :undefined, child | :restarting, :worker | :supervisor, :supervisor.modules()}
# inlining module() | :dynamic here because :supervisor.modules() is not exported
{term() | :undefined, child | :restarting, :worker | :supervisor, module() | :dynamic}
]
def which_children(supervisor) do
call(supervisor, :which_children)
+9 -11
View File
@@ -41,10 +41,10 @@ defmodule Supervisor.Spec do
end
end
Notice in this case we don't have to explicitly import
`Supervisor.Spec` as `use Supervisor` automatically does so.
Note that in this case we don't have to explicitly import
`Supervisor.Spec` since `use Supervisor` automatically does so.
Defining a module-based supervisor can be useful, for example,
to perform initialization tasks in the `c:init/1` callback.
to perform initialization tasks in the `c:Supervisor.init/1` callback.
## Supervisor and worker options
@@ -82,9 +82,9 @@ defmodule Supervisor.Spec do
terminates abnormally, i.e., with an exit reason other than
`:normal`, `:shutdown` or `{:shutdown, term}`
Notice that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
In this case the supervisor will only be restarted if its child specification was defined with
the `:restart` option is set to `:permanent` (the default).
Note that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
In this case the supervisor will only restart if its child specification was defined with
the `:restart` option set to `:permanent` (the default).
### Shutdown values (`:shutdown`)
@@ -136,7 +136,7 @@ defmodule Supervisor.Spec do
supervise and a set of `options`.
Returns a tuple containing the supervisor specification. This tuple can be
used as the return value of the `c:init/1` callback when implementing a
used as the return value of the `c:Supervisor.init/1` callback when implementing a
module-based supervisor.
## Examples
@@ -234,8 +234,7 @@ defmodule Supervisor.Spec do
function: atom,
modules: modules
) :: spec
# TODO: Deprecate on v1.11
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
def worker(module, args, options \\ []) do
child(:worker, module, args, options)
end
@@ -269,8 +268,7 @@ defmodule Supervisor.Spec do
function: atom,
modules: modules
) :: spec
# TODO: Deprecate on v1.11
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
def supervisor(module, args, options \\ []) do
options = Keyword.put_new(options, :shutdown, :infinity)
child(:supervisor, module, args, options)
+41 -19
View File
@@ -312,9 +312,14 @@ defmodule System do
"""
@spec user_home() :: String.t() | nil
def user_home do
{:ok, [[home] | _]} = :init.get_argument(:home)
encoding = :file.native_name_encoding()
:unicode.characters_to_binary(home, encoding, encoding)
case :init.get_argument(:home) do
{:ok, [[home] | _]} ->
encoding = :file.native_name_encoding()
:unicode.characters_to_binary(home, encoding, encoding)
_ ->
nil
end
end
@doc """
@@ -393,8 +398,9 @@ defmodule System do
@doc """
Registers 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.
Registers a function that will be invoked at the end of an Elixir script.
A script is typically started via the command line via the `elixir` and
`mix` executables.
The handler always executes in a different process from the one it was
registered in. As a consequence, any resources managed by the calling process
@@ -402,6 +408,9 @@ defmodule System do
function is invoked.
The function must receive the exit status code as an argument.
If the VM terminates programmatically, via `System.stop/1` or `System.halt/1`,
the `at_exit/1` callbacks are not executed.
"""
@spec at_exit((non_neg_integer -> any)) :: :ok
def at_exit(fun) when is_function(fun, 1) do
@@ -582,15 +591,20 @@ defmodule System do
`__STACKTRACE__/0` inside a rescue/catch. If you want to support
earlier Elixir versions, move `System.stacktrace/0` inside a rescue/catch.
Starting from Erlang/OTP 23, this function will always return an empty list.
Note that the Erlang VM (and therefore this function) does not
return the current stacktrace but rather the stacktrace of the
latest exception. To retrieve the stacktrace of the current process,
use `Process.info(self(), :current_stacktrace)` instead.
"""
# TODO: Fully deprecate it on Elixir v1.11 via @deprecated
# It is currently partially deprecated in elixir_dispatch.erl
def stacktrace do
apply(:erlang, :get_stacktrace, [])
# TODO: Once Erlang/OTP 23 is required, remove conditional, and update @doc accordingly.
# The warning is emitted by the compiler - so a @doc annotation is enough
@doc deprecated: "Use __STACKTRACE__ instead"
if function_exported?(:erlang, :get_stacktrace, 0) do
def stacktrace, do: apply(:erlang, :get_stacktrace, [])
else
def stacktrace, do: []
end
@doc """
@@ -623,6 +637,7 @@ defmodule System do
System.halt(:abort)
"""
@spec halt() :: no_return
@spec halt(non_neg_integer | binary | :abort) :: no_return
def halt(status \\ 0)
@@ -738,7 +753,12 @@ defmodule System do
* `:into` - injects the result into the given collectable, defaults to `""`
* `:cd` - the directory to run the command in
* `:env` - an enumerable of tuples containing environment key-value as binary
* `:env` - an enumerable of tuples containing environment key-value as
binary. The child process inherits all environment variables from its
parent process, the Elixir application, except those overwritten or
cleared using this option. Specify a value of `nil` to clear (unset) an
environment variable, which is useful for preventing credentials passed
to the application from leaking into child processes.
* `:arg0` - sets the command arg0
* `:stderr_to_stdout` - redirects stderr to stdout when `true`
* `:parallelism` - when `true`, the VM will schedule port tasks to improve
@@ -968,6 +988,7 @@ defmodule System do
Inlined by the compiler.
"""
@spec os_time() :: integer
@doc since: "1.3.0"
def os_time do
:os.system_time()
end
@@ -979,6 +1000,7 @@ defmodule System do
with no limitation and is not monotonic.
"""
@spec os_time(time_unit) :: integer
@doc since: "1.3.0"
def os_time(unit) do
:os.system_time(normalize_time_unit(unit))
end
@@ -987,6 +1009,7 @@ defmodule System do
Returns the Erlang/OTP release number.
"""
@spec otp_release :: String.t()
@doc since: "1.3.0"
def otp_release do
:erlang.list_to_binary(:erlang.system_info(:otp_release))
end
@@ -995,6 +1018,7 @@ defmodule System do
Returns the number of schedulers in the VM.
"""
@spec schedulers :: pos_integer
@doc since: "1.3.0"
def schedulers do
:erlang.system_info(:schedulers)
end
@@ -1003,6 +1027,7 @@ defmodule System do
Returns the number of schedulers online in the VM.
"""
@spec schedulers_online :: pos_integer
@doc since: "1.3.0"
def schedulers_online do
:erlang.system_info(:schedulers_online)
end
@@ -1071,15 +1096,12 @@ defmodule System do
end
defp warn(unit, replacement_unit) do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
stacktrace = Enum.drop(stacktrace, 3)
:elixir_config.warn({System, unit}, stacktrace) &&
IO.warn(
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
stacktrace
)
IO.warn_once(
{__MODULE__, unit},
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
_stacktrace_drop_levels = 4
)
replacement_unit
end
+149 -11
View File
@@ -203,6 +203,9 @@ defmodule Task do
a list `[pid_n, ..., pid2, pid1]` with at least one entry Where `pid_n` is
the PID that called the current process, `pid2` called `pid_n`, and `pid2` was
called by `pid1`.
If a task crashes, the callers field is included as part of the log message
metadata under the `:callers` key.
"""
@doc """
@@ -458,8 +461,8 @@ defmodule Task do
This is also useful when you're using the tasks for side effects.
Defaults to `true`.
* `:timeout` - the maximum amount of time (in milliseconds) each
task is allowed to execute for. Defaults to `5000`.
* `:timeout` - the maximum amount of time (in milliseconds or `:infinity`)
each task is allowed to execute for. Defaults to `5000`.
* `:on_timeout` - what to do when a task times out. The possible
values are:
@@ -488,6 +491,38 @@ defmodule Task do
stream = Task.async_stream(collection, Mod, :expensive_fun, [], ordered: false)
Stream.run(stream)
## Attention: async + take
Given items in an async stream are processed concurrently, doing
`async_stream` followed by `Enum.take/2` may cause more items than
requested to be processed. Let's see an example:
1..100
|> Task.async_stream(fn i ->
Process.sleep(100)
IO.puts(to_string(i))
end)
|> Enum.take(10)
For a machine with 8 cores, the above will process 16 items instead
of 10. The reason is that `async_stream/5` always have 8 elements
processing at once. So by the time `Enum` says it got all elements
it needed, there are still 6 elements left to be processed.
The solution here is to use `Stream.take/2` instead of `Enum.take/2`
to filter elements before-hand:
1..100
|> Stream.take(10)
|> Task.async_stream(fn i ->
Process.sleep(100)
IO.puts(to_string(i))
end)
|> Enum.to_list()
If for some reason you cannot take the elements before hand,
you can use `:max_concurrency` to limit how many elements
may be over processed at the cost of reducing concurrency.
"""
@doc since: "1.4.0"
@spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t()
@@ -558,11 +593,12 @@ defmodule Task do
In case the task process dies, the current process will exit with the same
reason as the task.
A timeout in milliseconds or `:infinity`, can be given with a default value of `5000`. If the
timeout is exceeded, then the current process will exit. If the task process
is linked to the current process which is the case when a task is started with
`async`, then the task process will also exit. If the task process is trapping
exits or not linked to the current process, then it will continue to run.
A timeout, in milliseconds or `:infinity`, can be given with a default value
of `5000`. If the timeout is exceeded, then the current process will exit. If
the task process is linked to the current process which is the case when a
task is started with `async`, then the task process will also exit. If the
task process is trapping exits or not linked to the current process, then it
will continue to run.
This function assumes the task's monitor is still active or the monitor's
`:DOWN` message is in the message queue. If it has been demonitored, or the
@@ -608,6 +644,109 @@ defmodule Task do
end
end
@doc """
Awaits replies from multiple tasks and returns them.
This function receives a list of tasks and waits for their replies in the
given time interval. It returns a list of the results, in the same order as
the tasks supplied in the `tasks` input argument.
If any of the task processes dies, the current process will exit with the
same reason as that task.
A timeout, in milliseconds or `:infinity`, can be given with a default value
of `5000`. If the timeout is exceeded, then the current process will exit.
Any task processes that are linked to the current process (which is the case
when a task is started with `async`) will also exit. Any task processes that
are trapping exits or not linked to the current process will continue to run.
This function assumes the tasks' monitors are still active or the monitors'
`:DOWN` message is in the message queue. If any tasks have been demonitored,
or the message already received, this function will wait for the duration of
the timeout.
This function can only be called once for any given task. If you want to be
able to check multiple times if a long-running task has finished its
computation, use `yield_many/2` instead.
## Compatibility with OTP behaviours
It is not recommended to `await` long-running tasks inside an OTP behaviour
such as `GenServer`. See `await/2` for more information.
## Examples
iex> tasks = [
...> Task.async(fn -> 1 + 1 end),
...> Task.async(fn -> 2 + 3 end)
...> ]
iex> Task.await_many(tasks)
[2, 5]
"""
@doc since: "1.11.0"
@spec await_many([t], timeout) :: [term]
def await_many(tasks, timeout \\ 5000) when is_timeout(timeout) do
awaiting =
for task <- tasks, into: %{} do
%Task{ref: ref, owner: owner} = task
if owner != self() do
raise ArgumentError, invalid_owner_error(task)
end
{ref, true}
end
timeout_ref = make_ref()
timer_ref =
if timeout != :infinity do
Process.send_after(self(), timeout_ref, timeout)
end
try do
await_many(tasks, timeout, awaiting, %{}, timeout_ref)
after
timer_ref && Process.cancel_timer(timer_ref)
receive do: (^timeout_ref -> :ok), after: (0 -> :ok)
end
end
defp await_many(tasks, _timeout, awaiting, replies, _timeout_ref)
when map_size(awaiting) == 0 do
for %{ref: ref} <- tasks, do: Map.fetch!(replies, ref)
end
defp await_many(tasks, timeout, awaiting, replies, timeout_ref) do
receive do
^timeout_ref ->
demonitor_pending_tasks(awaiting)
exit({:timeout, {__MODULE__, :await_many, [tasks, timeout]}})
{:DOWN, ref, _, proc, reason} when is_map_key(awaiting, ref) ->
demonitor_pending_tasks(awaiting)
exit({reason(reason, proc), {__MODULE__, :await_many, [tasks, timeout]}})
{ref, reply} when is_map_key(awaiting, ref) ->
Process.demonitor(ref, [:flush])
await_many(
tasks,
timeout,
Map.delete(awaiting, ref),
Map.put(replies, ref, reply),
timeout_ref
)
end
end
defp demonitor_pending_tasks(awaiting) do
Enum.each(awaiting, fn {ref, _} ->
Process.demonitor(ref, [:flush])
end)
end
@doc false
@deprecated "Pattern match directly on the message instead"
def find(tasks, {ref, reply}) when is_reference(ref) do
@@ -647,10 +786,9 @@ defmodule Task do
* the caller is trapping exits
A timeout, in milliseconds or `:infinity`, can be given with a default value
of `5000`. If the time runs out before a message from
the task is received, this function will return `nil`
and the monitor will remain active. Therefore `yield/2` can be
called multiple times on the same task.
of `5000`. If the time runs out before a message from the task is received,
this function will return `nil` and the monitor will remain active. Therefore
`yield/2` can be called multiple times on the same task.
This function assumes the task's monitor is still active or the
monitor's `:DOWN` message is in the message queue. If it has been
+6 -5
View File
@@ -102,8 +102,8 @@ defmodule Task.Supervised do
%{
label: {Task.Supervisor, :terminating},
report: %{
name: get_from(owner),
starter: self(),
name: self(),
starter: get_from(owner),
function: fun,
args: args,
reason: {log_value(kind, value), __STACKTRACE__}
@@ -112,7 +112,8 @@ defmodule Task.Supervised do
%{
domain: [:otp, :elixir],
error_logger: %{tag: :error_msg},
report_cb: &__MODULE__.format_report/1
report_cb: &__MODULE__.format_report/1,
callers: Process.get(:"$callers")
}
)
@@ -268,7 +269,7 @@ defmodule Task.Supervised do
receive do
# The task at position "position" replied with "value". We put the
# response in the "waiting" map and do nothing, since we'll only act on
# this response when the replying task dies (we'll notice in the :down
# this response when the replying task dies (we'll see this in the :down
# message).
{{^monitor_ref, position}, reply} ->
%{^position => {pid, :running}} = waiting
@@ -539,7 +540,7 @@ defmodule Task.Supervised do
# One of the spawned processes went down. We inform the parent process of
# this and keep going.
{:DOWN, ref, _, _, reason} ->
{:DOWN, ref, _, _, reason} when is_map_key(running_tasks, ref) ->
{task, running_tasks} = Map.pop(running_tasks, ref)
%{position: position, timer_ref: timer_ref, timed_out?: timed_out?} = task
+4 -2
View File
@@ -27,8 +27,10 @@ defmodule Task.Supervisor do
@typedoc "Option values used by `start_link`"
@type option ::
DynamicSupervisor.option()
| {:restart, :supervisor.restart()}
| {:shutdown, :supervisor.shutdown()}
# :permanent | :transient | :temporary here because :supervisor.restart() is not exported
| {:restart, :permanent | :transient | :temporary}
# :brutal_kill | timeout() here because :supervisor.shutdown() is not exported
| {:shutdown, :brutal_kill | timeout()}
@doc false
def child_spec(opts) when is_list(opts) do
+3 -3
View File
@@ -60,7 +60,7 @@ defmodule Tuple do
"""
@spec duplicate(term, non_neg_integer) :: tuple
def duplicate(data, size) do
def duplicate(data, size) when is_integer(size) and size >= 0 do
:erlang.make_tuple(size, data)
end
@@ -83,7 +83,7 @@ defmodule Tuple do
"""
@spec insert_at(tuple, non_neg_integer, term) :: tuple
def insert_at(tuple, index, value) do
def insert_at(tuple, index, value) when is_integer(index) and index >= 0 do
:erlang.insert_element(index + 1, tuple, value)
end
@@ -124,7 +124,7 @@ defmodule Tuple do
"""
@spec delete_at(tuple, non_neg_integer) :: tuple
def delete_at(tuple, index) do
def delete_at(tuple, index) when is_integer(index) and index >= 0 do
:erlang.delete_element(index + 1, tuple)
end
+1 -1
View File
@@ -204,7 +204,7 @@ defmodule URI do
next_pair =
case :binary.split(undecoded_next_pair, "=") do
[key, value] -> {decode_www_form(key), decode_www_form(value)}
[key] -> {decode_www_form(key), nil}
[key] -> {decode_www_form(key), ""}
end
{next_pair, rest}
+116 -201
View File
@@ -95,7 +95,9 @@ defmodule Version do
"""
import Kernel, except: [match?: 2]
defstruct [:major, :minor, :patch, :pre, :build]
@enforce_keys [:major, :minor, :patch]
defstruct [:major, :minor, :patch, :build, pre: []]
@type version :: String.t() | t
@type requirement :: String.t() | Version.Requirement.t()
@@ -116,36 +118,92 @@ defmodule Version do
for more information.
"""
defstruct [:source, :matchspec, :compiled]
defstruct [:source, :lexed]
@opaque t :: %__MODULE__{
source: String.t(),
matchspec: :ets.match_spec() | :ets.comp_match_spec(),
compiled: boolean
lexed: [atom | matchable]
}
@typep matchable ::
{Version.major(), Version.minor(), Version.patch(), Version.pre(), Version.build()}
@compile inline: [compare: 2]
@doc false
@spec new(String.t(), :ets.match_spec()) :: t
def new(source, spec) do
%__MODULE__{source: source, matchspec: spec, compiled: false}
end
@doc false
@spec compile(t) :: t
def compile(%__MODULE__{matchspec: spec} = requirement) do
%{requirement | matchspec: :ets.match_spec_compile(spec), compiled: true}
def new(source, lexed) do
%__MODULE__{source: source, lexed: lexed}
end
@doc false
@spec match?(t, tuple) :: boolean
def match?(%__MODULE__{matchspec: spec, compiled: true}, matchable_pattern) do
matches = :ets.match_spec_run([matchable_pattern], spec)
matches != []
def match?(%__MODULE__{lexed: lexed}, matchable_pattern) do
match_lexed?(lexed, matchable_pattern)
end
def match?(%__MODULE__{matchspec: spec, compiled: false}, matchable_pattern) do
{:ok, result} = :ets.test_ms(matchable_pattern, spec)
result != false
defp match_lexed?([operator, req, :&& | rest], version) do
match_op?(operator, req, version) and match_lexed?(rest, version)
end
defp match_lexed?([operator, req, :|| | rest], version) do
match_op?(operator, req, version) or match_lexed?(rest, version)
end
defp match_lexed?([operator, req], version) do
match_op?(operator, req, version)
end
defp match_op?(:==, req, version) do
compare(version, req) == :eq
end
defp match_op?(:!=, req, version) do
compare(version, req) != :eq
end
defp match_op?(:~>, {major, minor, nil, req_pre, _}, {_, _, _, pre, allow_pre} = version) do
compare(version, {major, minor, 0, req_pre, nil}) in [:eq, :gt] and
compare(version, {major + 1, 0, 0, [0], nil}) == :lt and
(allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:~>, {major, minor, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
compare(version, req) in [:eq, :gt] and
compare(version, {major, minor + 1, 0, [0], nil}) == :lt and
(allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:>, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
compare(version, req) == :gt and (allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:>=, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
compare(version, req) in [:eq, :gt] and (allow_pre or req_pre != [] or pre == [])
end
defp match_op?(:<, req, version) do
compare(version, req) == :lt
end
defp match_op?(:<=, req, version) do
compare(version, req) in [:eq, :lt]
end
defp compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do
cond do
major1 > major2 -> :gt
major1 < major2 -> :lt
minor1 > minor2 -> :gt
minor1 < minor2 -> :lt
patch1 > patch2 -> :gt
patch1 < patch2 -> :lt
pre1 == [] and pre2 != [] -> :gt
pre1 != [] and pre2 == [] -> :lt
pre1 > pre2 -> :gt
pre1 < pre2 -> :lt
true -> :eq
end
end
end
@@ -270,8 +328,12 @@ defmodule Version do
defp do_compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do
cond do
{major1, minor1, patch1} > {major2, minor2, patch2} -> :gt
{major1, minor1, patch1} < {major2, minor2, patch2} -> :lt
major1 > major2 -> :gt
major1 < major2 -> :lt
minor1 > minor2 -> :gt
minor1 < minor2 -> :lt
patch1 > patch2 -> :gt
patch1 < patch2 -> :lt
pre1 == [] and pre2 != [] -> :gt
pre1 != [] and pre2 == [] -> :lt
pre1 > pre2 -> :gt
@@ -344,12 +406,8 @@ defmodule Version do
@spec parse_requirement(String.t()) :: {:ok, Requirement.t()} | :error
def parse_requirement(string) when is_binary(string) do
case Version.Parser.parse_requirement(string) do
{:ok, spec} ->
requirement = Requirement.new(string, spec)
{:ok, requirement}
:error ->
:error
{:ok, lexed} -> {:ok, Requirement.new(string, lexed)}
:error -> :error
end
end
@@ -370,27 +428,20 @@ defmodule Version do
@doc since: "1.8.0"
@spec parse_requirement!(String.t()) :: Requirement.t()
def parse_requirement!(string) when is_binary(string) do
case Version.Parser.parse_requirement(string) do
{:ok, spec} ->
Requirement.new(string, spec)
:error ->
raise InvalidRequirementError, string
case parse_requirement(string) do
{:ok, requirement} -> requirement
:error -> raise InvalidRequirementError, string
end
end
@doc """
Compiles a requirement to its internal representation with
`:ets.match_spec_compile/1` for faster matching.
Compiles a requirement to an internal representation that may optimize matching.
The internal representation is opaque and cannot be converted to external
term format and then back again without losing its properties (meaning it
can not be sent to a process on another node and still remain a valid
compiled match_spec, nor can it be stored on disk).
The internal representation is opaque.
"""
@spec compile_requirement(Requirement.t()) :: Requirement.t()
def compile_requirement(requirement) do
Requirement.compile(requirement)
requirement
end
defp to_matchable(%Version{major: major, minor: minor, patch: patch, pre: pre}, allow_pre?) do
@@ -454,13 +505,27 @@ defmodule Version do
end
def lexer("", acc) do
Enum.reverse(acc)
Enum.map(Enum.reverse(acc), fn
op when is_atom(op) ->
op
version when is_binary(version) ->
case Version.Parser.parse_version(version, true) do
{:ok, version} -> version
:error -> :error
end
end)
end
@spec parse_requirement(String.t()) :: {:ok, term} | :error
def parse_requirement(source) do
lexed = lexer(source, [])
to_matchspec(lexed)
if valid_requirement?(lexed) do
{:ok, lexed}
else
:error
end
end
def parse_version(string, approximate? \\ false) when is_binary(string) do
@@ -550,7 +615,7 @@ defmodule Version do
defp valid_requirement?([a | next]), do: valid_requirement?(a, next)
# it must finish with a version
defp valid_requirement?(a, []) when is_binary(a) do
defp valid_requirement?(a, []) when is_tuple(a) do
true
end
@@ -560,179 +625,29 @@ defmodule Version do
end
# <version> or | <version> and
defp valid_requirement?(a, [b | next]) when is_binary(a) and is_atom(b) and b in [:||, :&&] do
defp valid_requirement?(a, [b | next]) when is_tuple(a) and is_atom(b) and b in [:||, :&&] do
valid_requirement?(b, next)
end
# or <version> | and <version>
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) and a in [:||, :&&] do
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_tuple(b) and a in [:||, :&&] do
valid_requirement?(b, next)
end
# ~> <version>
defp valid_requirement?(:~>, [b | next]) when is_tuple(b) do
valid_requirement?(b, next)
end
# <op> <version>
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) do
defp valid_requirement?(a, [{_major, _minor, patch, _pre, _build} = b | next])
when is_atom(a) and is_integer(patch) do
valid_requirement?(b, next)
end
defp valid_requirement?(_, _) do
false
end
defp approximate_upper(version) do
case version do
{major, _minor, nil, _} ->
{major + 1, 0, 0, [0]}
{major, minor, _patch, _} ->
{major, minor + 1, 0, [0]}
end
end
defp to_matchspec(lexed) do
if valid_requirement?(lexed) do
first = to_condition(lexed)
rest = Enum.drop(lexed, 2)
{:ok, [{{:"$1", :"$2", :"$3", :"$4", :"$5"}, [to_condition(first, rest)], [:"$_"]}]}
else
:error
end
catch
:invalid_matchspec -> :error
end
defp to_condition([:==, version | _]) do
matchable = parse_condition(version)
main_condition(:==, matchable)
end
defp to_condition([:!=, version | _]) do
matchable = parse_condition(version)
main_condition(:"/=", matchable)
end
defp to_condition([:~>, version | _]) do
from = parse_condition(version, true)
to = approximate_upper(from)
{
:andalso,
to_condition([:>=, matchable_to_string(from)]),
to_condition([:<, matchable_to_string(to)])
}
end
defp to_condition([:>, version | _]) do
{major, minor, patch, pre} = parse_condition(version)
{
:andalso,
{
:orelse,
main_condition(:>, {major, minor, patch}),
{:andalso, main_condition(:==, {major, minor, patch}), pre_condition(:>, pre)}
},
no_pre_condition(pre)
}
end
defp to_condition([:>=, version | _]) do
matchable = parse_condition(version)
{:orelse, main_condition(:==, matchable), to_condition([:>, version])}
end
defp to_condition([:<, version | _]) do
{major, minor, patch, pre} = parse_condition(version)
{
:orelse,
main_condition(:<, {major, minor, patch}),
{:andalso, main_condition(:==, {major, minor, patch}), pre_condition(:<, pre)}
}
end
defp to_condition([:<=, version | _]) do
matchable = parse_condition(version)
{:orelse, main_condition(:==, matchable), to_condition([:<, version])}
end
defp to_condition(current, []) do
current
end
defp to_condition(current, [:&&, operator, version | rest]) do
to_condition({:andalso, current, to_condition([operator, version])}, rest)
end
defp to_condition(current, [:||, operator, version | rest]) do
to_condition({:orelse, current, to_condition([operator, version])}, rest)
end
defp parse_condition(version, approximate? \\ false) do
case parse_version(version, approximate?) do
{:ok, {major, minor, patch, pre, _build}} -> {major, minor, patch, pre}
:error -> throw(:invalid_matchspec)
end
end
defp main_condition(op, version) when tuple_size(version) == 3 do
{op, {{:"$1", :"$2", :"$3"}}, {:const, version}}
end
defp main_condition(op, version) when tuple_size(version) == 4 do
{op, {{:"$1", :"$2", :"$3", :"$4"}}, {:const, version}}
end
defp pre_condition(:>, pre) do
length_pre = length(pre)
{
:orelse,
{:andalso, {:==, {:length, :"$4"}, 0}, {:const, length_pre != 0}},
{
:andalso,
{:const, length_pre != 0},
{
:orelse,
{:>, {:length, :"$4"}, length_pre},
{:andalso, {:==, {:length, :"$4"}, length_pre}, {:>, :"$4", {:const, pre}}}
}
}
}
end
defp pre_condition(:<, pre) do
length_pre = length(pre)
{
:orelse,
{:andalso, {:"/=", {:length, :"$4"}, 0}, {:const, length_pre == 0}},
{
:andalso,
{:"/=", {:length, :"$4"}, 0},
{
:orelse,
{:<, {:length, :"$4"}, length_pre},
{:andalso, {:==, {:length, :"$4"}, length_pre}, {:<, :"$4", {:const, pre}}}
}
}
}
end
defp no_pre_condition([]) do
{:orelse, :"$5", {:==, {:length, :"$4"}, 0}}
end
defp no_pre_condition(_pre) do
{:const, true}
end
defp matchable_to_string({major, minor, patch, pre}) do
patch = if patch, do: "#{patch}", else: "0"
pre = if pre != [], do: "-#{Enum.join(pre, ".")}"
"#{major}.#{minor}.#{patch}#{pre}"
end
end
end
@@ -8,12 +8,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.10 | Development
1.9 | Bug fixes and security patches
1.11 | Development
1.10 | Bug fixes and security patches
1.9 | Security patches only
1.8 | Security patches only
1.7 | Security patches only
1.6 | Security patches only
1.5 | Security patches only
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -53,7 +53,8 @@ Elixir version | Supported Erlang/OTP versions
1.7 | 19 - 22
1.8 | 20 - 22
1.9 | 20 - 22
1.10 | 21 - 22
1.10 | 21 - 22 (and Erlang/OTP 23 from v1.10.3)
1.11 | 21 - 23
While Elixir often adds compatibility to new Erlang/OTP versions on released branches, such as support for Erlang/OTP 20 in v1.4.5, those releases usually contain the minimum changes for Elixir to run without errors. Only the next minor release, in this case v1.5.0, does effectively leverage the new features provided by the latest Erlang/OTP release.
@@ -75,6 +76,10 @@ The first column is the version the feature was hard deprecated. The second colu
Version | Deprecated feature | Replaced by (available since)
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
[v1.11] | `Mix.Project.compile/2` | `Mix.Task.run("compile", args)` (v1.0)
[v1.11] | `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` | The new child specs outlined in `Supervisor` (v1.5)
[v1.11] | `Supervisor.start_child/2` and `Supervisor.terminate_child/2` | `DynamicSupervisor` (v1.6)
[v1.11] | `System.stacktrace/1` | `__STACKTRACE__` in `try/catch/rescue` (v1.7)
[v1.10] | `Code.ensure_compiled?/1` | `Code.ensure_compiled/1` (v1.0)
[v1.10] | `Code.load_file/2` | `Code.require_file/2` (v1.0) or `Code.compile_file/2` (v1.7)
[v1.10] | `Code.loaded_files/0` | `Code.required_files/0` (v1.7)
@@ -82,7 +87,6 @@ Version | Deprecated feature | Replaced by (ava
[v1.10] | Passing non-chardata to `Logger.log/2` | Explicitly convert to string with `to_string/1` (v1.0)
[v1.10] | `:compile_time_purge_level` in `Logger` app environment | `:compile_time_purge_matching` in `Logger` app environment (v1.7)
[v1.10] | `Supervisor.Spec.supervise/2` | The new child specs outlined in `Supervisor` (v1.5)
[v1.10] | `String.normalize/2` | `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` (Erlang/OTP 20)
[v1.10] | `:simple_one_for_one` strategy in `Supervisor` | `DynamicSupervisor` (v1.6)
[v1.10] | `:restart` and `:shutdown` in `Task.Supervisor.start_link/1` | `:restart` and `:shutdown` in `Task.Supervisor.start_child/3` (v1.6)
[v1.9] | Enumerable keys in `Map.drop/2`, `Map.split/2`, and `Map.take/2` | Call `Enum.to_list/1` on the second argument before hand (v1.0)
@@ -106,9 +110,7 @@ Version | Deprecated feature | Replaced by (ava
[v1.7] | `Registry.start_link/3` | `Registry.start_link/1` (v1.5)
[v1.7] | `Stream.chunk/2,3,4` | `Stream.chunk_every/2` and [`Stream.chunk_every/3,4`](`Stream.chunk_every/4`) (v1.5)
[v1.6] | `Enum.partition/2` | `Enum.split_with/2` (v1.4)
[v1.6] | `Keyword.replace/3` | `Keyword.fetch/2` + `Keyword.put/3` (v1.0)
[v1.6] | `Macro.unescape_tokens/1,2` | Use `Enum.map/2` to traverse over the arguments (v1.0)
[v1.6] | `Map.replace/3` | `Map.fetch/2` + `Map.put/3` (v1.0)
[v1.6] | `Module.add_doc/6` | [`@doc`](`Module`) module attribute (v1.0)
[v1.6] | `Range.range?/1` | Pattern match on [`_.._`](`Kernel.../2`) (v1.0)
[v1.5] | `()` to mean `nil` | `nil` (v1.0)
@@ -171,4 +173,5 @@ Version | Deprecated feature | Replaced by (ava
[v1.7]: https://github.com/elixir-lang/elixir/blob/v1.7/CHANGELOG.md#4-hard-deprecations
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
[v1.10]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-hard-deprecations
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
[v1.11]: https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md#4-hard-deprecations
@@ -8,7 +8,7 @@ You can create a new Elixir library by running the `mix new` command:
$ mix new my_library
The project name is given in the `snake_case` convention where all letters are lowercase and words are separate with underscores. This is the same convention used by variables, function names and atoms in Elixir. See the [Naming Conventions](naming-conventions.html) document for more information.
The project name is given in the `snake_case` convention where all letters are lowercase and words are separate with underscores. This is the same convention used by variables, function names and atoms in Elixir. See the [Naming Conventions](naming-conventions.md) document for more information.
Every project has a `mix.exs` file, with instructions on how to build, compile, run tests, and so on. Libraries commonly have a `lib` directory, which includes Elixir source code, and a `test` directory. A `src` directory may also exist for Erlang sources.
@@ -30,7 +30,7 @@ Writing code is only the first of many steps to publish a package. We strongly r
* Write tests. Elixir ships with a test-framework named [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html). The project generated by `mix new` includes sample tests and doctests.
* Write documentation. The Elixir community is proud of treating documentation as a first-class citizen and making documentation easily accessible. Libraries contribute to the status quo by providing complete API documentation with examples for their modules, types and functions. See the [Writing Documentation](writing-documentation.html) guide for more information. Projects like [ExDoc](https://github.com/elixir-lang/ex_doc) can be used to generate HTML and EPUB documents from the documentation. ExDoc also supports "extra pages", like this one that you are reading. Such pages augment the documentation with tutorials, guides and references.
* Write documentation. The Elixir community is proud of treating documentation as a first-class citizen and making documentation easily accessible. Libraries contribute to the status quo by providing complete API documentation with examples for their modules, types and functions. See the [Writing Documentation](writing-documentation.md) guide for more information. Projects like [ExDoc](https://github.com/elixir-lang/ex_doc) can be used to generate HTML and EPUB documents from the documentation. ExDoc also supports "extra pages", like this one that you are reading. Such pages augment the documentation with tutorials, guides and references.
Projects are often made available to other developers [by publishing a Hex package](https://hex.pm/docs/publish). Hex also [supports private packages for organizations](https://hex.pm/pricing). If ExDoc is configured for the Mix project, publishing a package on Hex will also automatically publish the generated documentation to [HexDocs](https://hexdocs.pm).
@@ -144,6 +144,34 @@ The application environment should be reserved only for configurations that are
For all remaining scenarios, libraries should not force their users to use the application environment for configuration. If the user of a library believes that certain parameter should be configured globally, then they can wrap the library functionality with their own application environment configuration.
### Avoid compile-time application configuration
Assuming you need to use the application configuration and you cannot avoid it as explained in the previous section, you should also avoid compile-time application configuration. For example, instead of doing this:
```elixir
@http_client Application.fetch_env!(:my_app, :http_client)
def request(path) do
@http_client.request(path)
end
```
you should do this:
```elixir
def request(path) do
http_client().request(path)
end
defp http_client() do
Application.fetch_env!(:my_app, :http_client)
end
```
That's because by reading the application in the module body and storing it in a module attribute, we are effectively reading the configuration at compile-time, which may become an issue when configuring the system later.
If, for some reason, you must read the application environment at compile time, use `Application.compile_env/2`. Read [the "Compile-time environment" section of the Application docs](Application.html#module-compile-time-environment) for more information.
### Avoid `use` when an `import` is enough
A library should not provide `use MyLib` functionality if all `use MyLib` does is to `import`/`alias` the module itself. For example, this is an anti-pattern:
@@ -71,7 +71,7 @@ More examples of paired functions: `Base.decode16/2` and `Base.decode16!/2`, `Fi
There are also some non-paired functions, with no non-bang variant. The bang still signifies that it will raise an exception on failure. Example: `Protocol.assert_protocol!/1`.
In macro code, the bang on `Kernel.alias!/1` and `Kernel.var!/2` signifies that [macro hygiene](https://elixir-lang.org/getting-started/meta/macros.html#macros-hygiene) is set aside.
In macro code, the bang on `Kernel.alias!/1` and `Kernel.var!/2` signifies that [macro hygiene](https://elixir-lang.org/getting-started/meta/macros.html#macro-hygiene) is set aside.
## Trailing question mark (`foo?`)
@@ -89,7 +89,7 @@ Examples: `Integer.is_even/1`, `Kernel.is_list/1`
These functions and macros follow the Erlang convention of an `is_` prefix, instead of a trailing question mark, precisely to indicate that they are allowed in guard clauses.
Note that type checks that are not valid in guard clauses do not follow this convention. Examples: `Keyword.keyword?/1`, `Regex.regex?/1`
Note that type checks that are not valid in guard clauses do not follow this convention. For example: `Keyword.keyword?/1`.
## Special names
@@ -13,7 +13,7 @@ Operator
`+` `-` `!` `^` `not` `~~~` | Unary
`*` `/` | Left to right
`+` `-` | Left to right
`++` `--` `..` `<>` | Right to left
`++` `--` `..` `<>` `+++` `---` | Right to left
`^^^` | Left to right
`in` `not in` | Left to right
`\|>` `<<<` `>>>` `<<~` `~>>` `<~` `~>` `<~>` `<\|>` | Left to right
@@ -29,18 +29,55 @@ Operator
`when` | Right to left
`<-` `\\` | Left to right
## General operators
Elixir provides the following built-in operators that are defined as functions that can be overridden:
* [`+`](`+/1`) and [`-`](`-/1`) - unary positive/negative
* [`+`](`+/2`), [`-`](`-/2`), [`*`](`*/2`), and [`/`](`//2`) - basic arithmetic operations
* [`++`](`++/2`) and [`--`](`--/2`) - list concatenation and subtraction
* [`and`](`and/2`) and [`&&`](`&&/2`) - strict and relaxed boolean "and"
* [`or`](`or/2`) and [`||`](`||/2`) - strict and relaxed boolean "or"
* [`not`](`not/1`) and [`!`](`!/1`) - strict and relaxed boolean "not"
* [`in`](`in/2`) and [`not in`](`in/2`) - membership
* [`@`](`@/1`) - module attribute
* [`..`](`../2`) - range creation
* [`<>`](`<>/2`) - binary concatenation
* [`|>`](`|>/2`) - pipeline
* [`=~`](`=~/2`) - text-based match
Many of those can be used in guards; consult the [list of allowed guard functions and operators](patterns-and-guards.md#list-of-allowed-functions-and-operators).
Additionally, there are a few other operators that Elixir parses but doesn't actually use.
See [Custom and overridden operators](#custom-and-overridden-operators) below for a list and for guidelines about their use.
Some other operators are special forms and cannot be overridden:
* [`^`](`^/1`) - pin operator
* [`.`](`./2`) - dot operator
* [`=`](`=/2`) - match operator
* [`&`](`&/1`) - capture operator
* [`::`](`Kernel.SpecialForms.::/2`) - type operator
Finally, these operators appear in the precedence table above but are only meaningful within certain constructs:
* `=>` - see [`%{}`](`%{}/1`)
* `when` - see [Guards](patterns-and-guards.md#guards)
* `<-` - see [`for`](`for/1`) and [`with`](`with/1`)
* `\\` - see [Default arguments](Kernel.html#def/2-default-arguments)
## Comparison operators
Elixir provides the following built-in comparison operators:
Elixir provides the following built-in comparison operators (all of which can be used in guards):
* [`==`](`==/2`) - equality
* [`===`](`===/2`) - strict equality
* [`!=`](`!=/2`) - inequality
* [`!==`](`!==/2`) - strict inequality
* [`<`](`</2`) - less than
* [`>`](`>/2`) - greater than
* [`<=`](`<=/2`) - less than or equal
* [`>=`](`>=/2`) - greater than or equal
* [`==`](`==/2`) - equal to
* [`===`](`===/2`) - strictly equal to
* [`!=`](`!=/2`) - inequal to
* [`!==`](`!==/2`) - strictly inequal to
* [`<`](`</2`) - less-than
* [`>`](`>/2`) - greater-than
* [`<=`](`<=/2`) - less-than or equal to
* [`>=`](`>=/2`) - greater-than or equal to
The only difference between [`==`](`==/2`) and [`===`](`===/2`) is that [`===`](`===/2`) is strict when it comes to comparing integers and floats:
@@ -76,6 +113,7 @@ The collection types are compared using the following rules:
* Maps are compared by size, then by keys in ascending term order, then by values in key order. In the specific case of maps' key ordering, integers are always considered to be less than floats.
* Lists are compared element by element.
* Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit.
* Atoms are compared using their string value, codepoint by codepoint.
## Custom and overridden operators
@@ -107,7 +145,6 @@ iex> 1 <~ 2
The following is a table of all the operators that Elixir is capable of parsing, but that are not used by default:
* `|`
* `|||`
* `&&&`
* `<<<`
@@ -119,6 +156,8 @@ The following is a table of all the operators that Elixir is capable of parsing,
* `<~>`
* `<|>`
* `^^^`
* `+++`
* `---`
* `~~~`
The following operators are used by the `Bitwise` module when imported: [`&&&`](`Bitwise.&&&/2`), [`^^^`](`Bitwise.^^^/2`), [`<<<`](`Bitwise.<<</2`), [`>>>`](`Bitwise.>>>/2`), [`|||`](`Bitwise.|||/2`), [`~~~`](`Bitwise.~~~/1`). See the documentation for `Bitwise` for more information.
@@ -6,7 +6,7 @@ This page describes the semantics of patterns and guards, where they are all all
## Patterns
Patterns in Elixir are made of variables, literals, and data-structure specific syntax. One of the most used constructs to perform pattern matching is the match operator (`=`):
Patterns in Elixir are made of variables, literals, and data-structure specific syntax. One of the most used constructs to perform pattern matching is the match operator ([`=`](`=/2`)):
```iex
iex> x = 1
@@ -83,7 +83,7 @@ Atoms and numbers (integers and floats) can appear in patterns and they are alwa
```iex
iex> :atom = :atom
:atotm
:atom
iex> :atom = :another_atom
** (MatchError) no match of right hand side value: :another_atom
```
@@ -156,7 +156,7 @@ iex> [head | tail] = []
** (MatchError) no match of right hand side value: []
```
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator (`++`):
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator ([`++`](`++/2`)):
```elixir
iex> 'hello ' ++ world = 'hello world'
@@ -207,7 +207,7 @@ Finally, note map keys in patterns must always be literals or previously bound v
### Binaries
Binaries may appear in patterns using the double less-than/greater-than syntax (`<<>>`). A binary in a pattern can match multiple segments at the same, each with different type, size, and unit:
Binaries may appear in patterns using the double less-than/greater-than syntax ([`<<>>`](`<<>>/1`)). A binary in a pattern can match multiple segments at the same, each with different type, size, and unit:
```iex
iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
@@ -216,9 +216,9 @@ iex> val
31544
```
See the documentation for `<<>>` for a complete definition of pattern matching for binaries.
See the documentation for [`<<>>`](`<<>>/1`) for a complete definition of pattern matching for binaries.
Finally, remember that strings in Elixir are UTF-8 encoded binaries. This means that, similar to charlists, prefix matches on strings are also possible with the binary concatenation operator (`<>`):
Finally, remember that strings in Elixir are UTF-8 encoded binaries. This means that, similar to charlists, prefix matches on strings are also possible with the binary concatenation operator ([`<>`](`<>/2`)):
```elixir
iex> "hello " <> world = "hello world"
@@ -231,7 +231,7 @@ Suffix matches (`hello <> " world"`) are not valid patterns.
## Guards
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and ohers.
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and others.
Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere. It also allows the compiler to optimize the code related to guards efficiently.
@@ -282,7 +282,7 @@ not_nil_head?(["some_value", "another_value"])
Even though the head of the list is not `nil`, the first clause for `not_nil_head?/1` fails because the expression does not evaluate to `true`, but to `"some_value"`, therefore triggering the second clause which returns `false`. To make the guard behave correctly, you must ensure that the guard evaluates to `true`, like so:
```elixir
def not_nil_head?(term) when head != nil, do: true
def not_nil_head?([head | _]) when head != nil, do: true
def not_nil_head?(_), do: false
not_nil_head?(["some_value", "another_value"])
@@ -309,7 +309,7 @@ iex> case "hello" do
...> _anything_else ->
...> :failed
...> end
:worked
:failed
```
In many cases, we can take advantage of this. In the code above, we used `tuple_size/1` to both check that the given value is a tuple *and* check its size (instead of using `is_tuple(something) and tuple_size(something) == 2`).
@@ -380,13 +380,7 @@ Check.empty?({})
## Where patterns and guards can be used
In the examples above, we have used the match operator (`=`) and function clauses to showcase patterns and guards respectively. Here is the list of the built-in constructs in Elixir that support patterns and guards.
* the match operator (`=`) (exceptionally does not support guards):
```elixir
{:ok, binary} = File.read("some/file")
```
In the examples above, we have used the match operator ([`=`](`=/2`)) and function clauses to showcase patterns and guards respectively. Here is the list of the built-in constructs in Elixir that support patterns and guards.
* `match?/2`:
@@ -430,8 +424,16 @@ In the examples above, we have used the match operator (`=`) and function clause
* [`try`](`try/1`) supports patterns and guards on `catch` and `else`
* [`receive`](`receive/1`) supports patterns and guards to match on the received messages.
* custom guards can also be defined with `defguard/1` and `defguardp/1`. A custom guard can only be defined based on existing guards.
Note that the match operator ([`=`](`=/2`)) does *not* support guards:
```elixir
{:ok, binary} = File.read("some/file")
```
## Custom patterns and guards expressions
Only the constructs listed in this page are allowed in patterns and guards. However, we can take advantage of macros to write custom patterns guards that can simplify our programs or make them more domain-specific. At the end of the day, what matters is that the *output* of the macros boils down to a combination of the constructs above.
@@ -21,7 +21,7 @@ Integers (`1234`) and floats (`123.4`) in Elixir are represented as a sequence o
### Atoms
Unquoted atoms start with a colon (`:`) which must be immediately followed by an underscore or a Unicode letter. The atom may continue using a sequence of Unicode letters, numbers, underscores, and `@`. Atoms may end in `!` or `?`. See [Unicode Syntax](unicode-syntax.html) for a formal specification. Valid unquoted atoms are: `:ok`, `:ISO8601`, and `:integer?`.
Unquoted atoms start with a colon (`:`) which must be immediately followed by an underscore or a Unicode letter. The atom may continue using a sequence of Unicode letters, numbers, underscores, and `@`. Atoms may end in `!` or `?`. See [Unicode Syntax](unicode-syntax.md) for a formal specification. Valid unquoted atoms are: `:ok`, `:ISO8601`, and `:integer?`.
If the colon is immediately followed by a pair of double- or single-quotes surrounding the atom name, the atom is considered quoted. In contrast with an unquoted atom, this one can be made of any Unicode character (not only letters), such as `:'🌢 Elixir'`, `:"++olá++"`, and `:"123"`.
@@ -84,27 +84,27 @@ Structs built on the map syntax by passing the struct name between `%` and `{`.
### Variables
Variables in Elixir must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The variable may continue using a sequence of Unicode letters, numbers, and underscores. Variables may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
Variables in Elixir must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The variable may continue using a sequence of Unicode letters, numbers, and underscores. Variables may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
[Elixir's naming conventions](naming-conventions.html) recommend variables to be in `snake_case` format.
[Elixir's naming conventions](naming-conventions.md) recommend variables to be in `snake_case` format.
### Non-qualified calls (local calls)
Non-qualified calls, such as `add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscore. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
Non-qualified calls, such as `add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscore. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
Parentheses for non-qualified calls are optional, except for zero-arity calls, which would then be ambiguous with variables. If parentheses are used, they must immediately follow the function name *without spaces*. For example, `add (1, 2)` is a syntax error, since `(1, 2)` is treated as an invalid block which is attempted to be given as a single argument to `add`.
[Elixir's naming conventions](naming-conventions.html) recommend calls to be in `snake_case` format.
[Elixir's naming conventions](naming-conventions.md) recommend calls to be in `snake_case` format.
### Operators
As many programming languages, Elixir also support operators as non-qualified calls with their precedence and associativity rules. Constructs such as `=`, `when`, `&` and `@` are simply treated as operators. See [the Operators page](operators.html) for a full reference.
As many programming languages, Elixir also support operators as non-qualified calls with their precedence and associativity rules. Constructs such as `=`, `when`, `&` and `@` are simply treated as operators. See [the Operators page](operators.md) for a full reference.
### Qualified calls (remote calls)
Qualified calls, such as `Math.add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscores. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
Qualified calls, such as `Math.add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscores. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
[Elixir's naming conventions](naming-conventions.html) recommend calls to be in `snake_case` format.
[Elixir's naming conventions](naming-conventions.md) recommend calls to be in `snake_case` format.
For qualified calls, Elixir also allows the function name to be written between double- or single-quotes, allowing calls such as `Math."++add++"(1, 2)`. Operators can be used as qualified calls without a need for quote, such as `Kernel.+(1, 2)`.
@@ -114,7 +114,7 @@ Parentheses for qualified calls are optional. If parentheses are used, they must
Aliases are constructs that expand to atoms at compile-time. The alias `String` expands to the atom `:"Elixir.String"`. Aliases must start with an ASCII uppercase character which may be followed by any ASCII letter, number, or underscore. Non-ASCII characters are not supported in aliases.
[Elixir's naming conventions](naming-conventions.html) recommend aliases to be in `CamelCase` format.
[Elixir's naming conventions](naming-conventions.md) recommend aliases to be in `CamelCase` format.
### Blocks
@@ -234,7 +234,7 @@ end
#=> {:+, [], [1, 2]}
```
Notice that `.` is also an operator. Remote calls use the dot in the AST with two arguments, where the second argument is always an atom:
Note that `.` is also an operator. Remote calls use the dot in the AST with two arguments, where the second argument is always an atom:
```elixir
quote do
@@ -104,6 +104,7 @@ The following literals are also supported in typespecs:
## Maps
| %{} # empty map
| %{key: value_type} # map with required key :key of value_type
| %{key_type => value_type} # map with required pairs of key_type and value_type
| %{required(key_type) => value_type} # map with required pairs of key_type and value_type
| %{optional(key_type) => value_type} # map with optional pairs of key_type and value_type
| %SomeStruct{} # struct with all fields of any type
@@ -162,7 +163,7 @@ A map value does not belong to this type if it contains a key that is not in the
If you want to denote that keys that were not previously defined in the map are allowed,
it is common to end a map type with `optional(any) => any`.
Notice that the syntactic representation of `map()` is `%{optional(any) => any}`, not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
Note that the syntactic representation of `map()` is `%{optional(any) => any}`, not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
### User-defined types
@@ -259,7 +260,7 @@ Elixir's standard library contains a few frequently used behaviours such as `Gen
### Inspecting behaviours
The `@callback` and `@optional_callback` attributes are used to create a `behaviour_info/1` function available on the defining module. This function can be used to retrieve the callbacks and optional callbacks defined by that module.
The `@callback` and `@optional_callbacks` attributes are used to create a `behaviour_info/1` function available on the defining module. This function can be used to retrieve the callbacks and optional callbacks defined by that module.
For example, for the `MyBehaviour` module defined in "Optional callbacks" above:

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