Compare commits

..
185 Commits
Author SHA1 Message Date
José Valim 327063cc84 Release v1.16.3 2024-05-21 00:14:25 +02:00
José Valim 795d0a9583 More fixes 2024-05-20 16:51:13 +02:00
José Valim 1150ebad03 Update CHANGELOG 2024-05-06 12:50:25 +02:00
José Valim 18feec4cd8 Add brackets around keyword lists when formatting with when, closes #13503 2024-05-06 12:44:54 +02:00
José Valim 7d5920206b Check that docs are not hidden in suite, closes #13516 2024-05-02 01:26:48 +02:00
José Valim a694447601 Ensure translators are persisted across logger restarts 2024-05-01 11:40:12 +02:00
José Valim 19ae388dda Add .bat/.com disclaimers to System.cmd and Port 2024-04-17 11:29:02 +02:00
José Valim 6b5edea5f1 Fix --dbg handling in bin/elixir, closes #13482 2024-04-08 09:31:20 +02:00
Arne Bedurftig b736c23af9 Fix typo in basic types (#13481)
The exclamation mark was missing in the result of the concatenation.
2024-04-07 20:58:08 +02:00
Theodor Fiedler 63f0000ace Translate :undefined URI port to nil (#13464)
Resolves #13462

When a url string is schema less and the host is followed by a colon
without setting the actual port, `:uri_string.parse/1` returns the
port to be `:undefined`. As long as the url string is parseable,
we should translate `:undefined` to `nil`, in order to ensure we return
a valid URI struct.
2024-04-04 12:43:16 +02:00
José Valim 5e8b187d08 Ensure compile paths are available during compilation, closes #13458 2024-04-01 12:18:01 +02:00
Jean Klingler aea1a47b8d Only infer size in pinned variable when needed (#13423) 2024-03-20 19:00:17 +09:00
Jean Klingler eeba8e992d Fix Enum.slide/3 example in cheatsheet (#13054) 2024-03-16 17:09:52 +01:00
Steven C dd42dd8620 Fix typo around Enum.slide/3 in the Enum cheatsheet (#13053) 2024-03-16 17:09:45 +01:00
José Valim ed1ddfd338 Add parens to private macro example, closes #13411 2024-03-13 12:28:36 +01:00
José Valim d17df054e2 Skip tests if Erlang was compiled without docs, closes #13322 2024-03-10 19:07:22 +01:00
José Valim 41f6ee3f2e Release v1.16.2 2024-03-10 11:54:45 +01:00
José Valim 9d31768f42 Update CHANGELOG 2024-03-07 11:25:06 +01:00
José Valim 558e0ba6a6 Discard mermaid fenced blocks from ansi docs 2024-03-07 11:06:07 +01:00
Jean Klingler ab1e3448f0 Replace deprecated :code.lib_dir/2 usages (#13395) 2024-03-06 13:00:19 +01:00
Jean Klingler 17cd103a28 Logger handles :process_label from OTP27 (#13392) 2024-03-06 13:00:19 +01:00
José Valim 60e6ceae10 Correct task link in docs, closes #13388 2024-03-06 12:59:20 +01:00
José Valim f64ccd0871 Improve capture_log docs with latest Logger 2024-03-05 18:37:17 +01:00
Jonatan Kłosko 6c463b2310 Ensure install-related functions do not crash when Mix is not started (#13391) 2024-03-04 20:45:03 +01:00
H e310dd91d6 Remove duplicate line in formatter_callback doc (#13387) 2024-03-04 15:46:47 +09:00
Jonatan Kłosko 29ef147a22 Remove consolidated when restoring Mix.install/2 dir (#13382) 2024-03-01 09:34:26 +01:00
Jonatan Kłosko 74ab38439e Fix Mix.install_project_dir/0 spec (#13381) 2024-03-01 09:34:26 +01:00
José Valim 5654752026 Add @doc since to install_project_dir 2024-02-29 20:30:48 +01:00
Jonatan Kłosko e78538a42b Add environment variable for reusing Mix.install/2 installation (#13378) 2024-02-29 19:20:17 +01:00
Jonatan Kłosko 079b92e98e Support recompiling local Mix.install/2 dependencies (#13375) 2024-02-29 08:41:11 +01:00
José Valim 325877a224 Clarify the meaning of --overwrite, closes #13371 2024-02-26 20:16:27 +01:00
José Valim 097c4cf672 Emit defmodule tracing event 2024-02-26 18:28:47 +01:00
Łukasz Samson fe16fd3442 Add documentation to Exception callbacks (#13367) 2024-02-24 16:23:16 +01:00
Jean Klingler e520e8ce96 Fix unexpected rounding signs on OPT26- (#13365) 2024-02-24 16:05:09 +01:00
Jean Klingler dd004539c6 Fixes to support 0TP27 (#13351)
* Fix non-deterministic key-value tests

* Fix non-deterministic Enum tests

* Fix :only option when deriving Inspect

* Float.ceil and Float.floor return -0.0 for negative numbers

* Fix non-deterministic Registry doctest

* Simplify check
2024-02-24 16:03:58 +01:00
Jean Klingler 2512cbaff0 Fix doctest example since Inspect.MapSet changed (#13366) 2024-02-24 22:21:18 +09:00
sabiwara a99a9cf47a Fix test due to backport 2024-02-24 00:11:43 +09:00
Jean Klingler 677ad618e9 Fix charlist formatting issue on '\"' (#13364) 2024-02-24 00:02:38 +09:00
Philip Munksgaard ff309747b7 Escape pinned values when computing diff (#13354)
Take these two tests:

```elixir
  test "correctly colored" do
    assert [{:foo}] = [{:bar}]
  end

  test "incorrectly colored" do
    val = [{:foo}]
    assert ^val = [{:bar}]
  end
```

That's because when diffing a pin, we were not converting the underlying
diff context from match to ===.

This fixes #13348
2024-02-17 12:51:27 +01:00
José Valim e5ec5ce7f6 Add API for deleting SCM
This is used by Hex when its application is stopped
to revert its changes to Mix state.
2024-02-15 12:22:53 +01:00
Jean Klingler f3e669c471 Fix :trim_doc example in doc (#13340) 2024-02-13 07:39:31 +09:00
Jean Klingler bf915aa2e7 Fix <> match test (#13327) 2024-02-09 17:03:42 +09:00
Jean KlinglerandJosé Valim ef3e2796ef Update argument error message when matching with <> (#13325)
* Update argument error message when matching with <>

* Update lib/elixir/lib/kernel.ex

Co-authored-by: José Valim <jose.valim@gmail.com>

---------

Co-authored-by: José Valim <jose.valim@gmail.com>
2024-02-09 16:50:08 +09:00
Mitchell Hanberg d8cc841ab0 Include from_brackets metadata in all cases (#13317) 2024-02-05 09:49:40 +01:00
José Valim 7098ac8a16 Preserve . semantics in Path.relative_to, closes #13310 2024-02-01 12:41:26 +01:00
José Valim e0658bb55a Release v1.16.1 2024-01-31 10:24:07 +01:00
José Valim cf8c28c34b Fix autocompletion on Erlang/OTP 26, closes #13307 2024-01-31 10:12:58 +01:00
José Valim 60dcd14390 Fix Rebar3 env var with spaces (#13303) 2024-01-30 20:21:33 +01:00
Steve Johns 8344e218a6 docs: fix grammar errors in syntax reference docs (#13302) 2024-01-30 19:57:23 +01:00
José Valim 740b2d74df Escape rebar3 paths 2024-01-29 10:19:10 +01:00
José Valim da0189e641 Fix docs link
Related to #13284.
2024-01-27 11:46:53 +01:00
José Valim ad778a6f29 Fix capitalize for single codepoint (#13268) 2024-01-19 19:43:03 +01:00
José Valim c35edd1ee8 Improve end_of_expression docs 2024-01-18 08:14:04 +09:00
José Valim bb8ad4406b Improve ast metadata docs 2024-01-18 08:13:57 +09:00
Jean Klingler 111b48dcf4 Resolve relative paths in exunit filter (#13258) 2024-01-18 08:12:32 +09:00
Mitchell Hanberg 6dccefe687 Fix :from_interpolation docs (#13251) 2024-01-18 08:12:03 +09:00
Roman 9ea950c9af Macro anti-patterns: fix incorrect error message in code examples (#13259) 2024-01-18 08:09:13 +09:00
José Valim 7d100fff9f Always log errors at the end of compilation 2024-01-17 23:16:04 +01:00
Roman 6b69c7f5ac Fix the explanation to match the explained code example in docs (#13255)
The description incorrectly states that both processes are initialized with 0, while in the code the second process receives a non-default initial value.
2024-01-15 13:42:51 +01:00
Jean Klingler 691d402341 Fix Code.Normalizer for keyword operand with :do key (#13250) 2024-01-13 17:59:16 +09:00
José Valim b96211a325 Do not assume there is a $HOME, closes #13127 2024-01-10 13:01:55 +01:00
José Valim 17b9ba61d9 Do not crash parallel compiler on external reports, closes #13224 2024-01-10 10:17:03 +01:00
Hussien Liban f83b4e4e84 Update design-anti-patterns.md (#13210)
Not setting an option returns just the integer
2024-01-08 20:39:08 +09:00
Adebisi AdeyeyeandAdebisi Adeyeye 4de331597a Fix typo in docs: initial_valye -> initial_value (#13211)
Co-authored-by: Adebisi Adeyeye <adebisi.adeyeye@proebb.com>
2024-01-08 20:39:08 +09:00
Tomás Grüner 6e295998ef Fix typo in docs: Keywoird -> Keyword (#13205) 2024-01-08 20:39:08 +09:00
Will DouglasandWill Douglas f2dae095f8 Fix typo in docs: for -> force (#13215)
Co-authored-by: Will Douglas <will.cavalcanti@vmtecnologia.io>
2024-01-08 20:39:08 +09:00
Jean Klingler 6f5715fc30 Replace single quotes in charlist in doc (#13233) 2024-01-08 20:39:08 +09:00
José Valim 1ece71a831 Handle Windows separators on mix test (#13232)
Closes #13225.
2024-01-07 23:07:20 +01:00
Ryan B. Harvey 197351dd73 Fix a comment typo in the Code Anti-Patterns doc page (#13219) 2024-01-02 18:03:37 +01:00
José Valim 0d671dafea Improve anti-pattern titles 2024-01-02 16:42:17 +01:00
José Valim 9ae7c39125 Update CHANGELOG, closes #13217 2023-12-31 18:20:20 +01:00
José Valim ba8fb4dff1 Improve yecc/leex warnings, closes #13213 2023-12-28 17:12:51 +01:00
José Valim 194661197e Add more examples to app config anti-pattern (#13204) 2023-12-26 09:57:32 +01:00
Travis Vander Hoop 298acd1cdb Fix typos and tweak language in anti-pattern docs (#13208) 2023-12-26 09:57:32 +01:00
Artem Solomatin 97c608c346 Fix typo in design-anti-patterns doc (#13207) 2023-12-26 09:57:32 +01:00
José Valim f84bc19bf2 Additional clarity on long list of parameters anti-patterns 2023-12-24 14:54:45 +01:00
Alex Martsinovich 91375778cb Fix example in non-assertive map access antipattern (#13201) 2023-12-23 10:31:57 +01:00
José Valim 38ddc98fef Release v1.16.0 2023-12-22 17:45:12 +01:00
José Valim 1b7c73273d Last pass over anti-patterns 2023-12-22 14:31:44 +01:00
José Valim 5bd5e75c3b Extract snippet in elixir_errors to simplify exception handling 2023-12-21 21:14:55 +01:00
José Valim 1cba584ac8 Fix column precision in test 2023-12-21 19:48:44 +01:00
José Valim dfafea092e Consider column in snippets, closes #13199 2023-12-21 19:30:15 +01:00
José Valim 0ae4bfb222 Preserve diagnostics based on source field, closes #13142 2023-12-21 17:22:43 +01:00
José Valim 7db4413476 Add source field to diagnostics 2023-12-21 17:22:43 +01:00
José Valim 4d52d18ef7 Do not reset column state in tests 2023-12-21 17:22:43 +01:00
José Valim 39ff86c551 Indent lists 2023-12-21 15:47:59 +01:00
Tobias Pfeiffer 12fdd8ea9b Document the process anti pattern of sending large data (#13194)
Follow up to/extension of #13173
2023-12-21 15:47:59 +01:00
José Valim 28f8eba9a2 Improve docs for URI.encode/2 2023-12-17 13:05:48 +01:00
José Valim b3b14202d9 Normalize exception handling in diagnostics 2023-12-17 11:40:49 +01:00
José Valim ffa3ef1ccf Remove unused function 2023-12-14 17:19:58 +01:00
José Valim d13f4155af Do not warn unused imports twice, closes #13178 2023-12-14 17:19:58 +01:00
José Valim d23e42e27f Normalize token missing and mismatched delimiter exceptions
Closes #13183.
Closes #13185.
Closes #13186.
Closes #13187.
2023-12-14 17:07:44 +01:00
José Valim 872efb180d Document diagnostic span 2023-12-14 12:07:20 +01:00
José Valim 2e61f32b6d Update CHANGELOG 2023-12-14 11:23:56 +01:00
José Valim 7abb3bdddc Unify position handling and improve docs
See #13179.
See #13184.
2023-12-14 11:15:05 +01:00
Vinícius Müller 67fd9824cc Improve diagnostics for unclosed heredocs (#13182) 2023-12-14 09:24:59 +01:00
José Valim 1878e30bdb Improve docs and support column in IO.warn, closes #13179 2023-12-13 23:31:49 +01:00
Vinícius Müller 88bbffd614 Add info about unclosed delimiters diagnostic to 1.16 changelog (#13180) 2023-12-13 23:31:38 +01:00
Maarten van Vliet a50a2fd983 Fix typo (#13181) 2023-12-14 07:17:11 +09:00
Tobias Pfeiffer c950a57a3e Fix top level functions changelog (enabled --> disabled) (#13177) 2023-12-13 22:56:19 +09:00
Tobias Pfeiffer 80404da8f6 Mention the fixed regression (#13176)
As best as I learned this was introduced in #11420 and fixed in aabe465
It shouldn't affect almost anyone except for scripting usage of
elixir and (probably most notably) benchee benchmarks that don't
call functions in a module.
2023-12-13 14:36:33 +01:00
José Valim 010d516b11 Fix indentation in mix release 2023-12-13 11:56:41 +01:00
José Valim fb42733873 Improve anonymous functions guide 2023-12-13 06:43:20 +01:00
Tobias Pfeiffer 1070b2f434 Mention dangers around Task and sending a lot of data along (#13173)
The `Task` module is one of the coolest modules in elixir and
is probably the first contact and experience of a lot of
beginners with parallelism in elixir. I hence find it worthwhile
to warn about the memory copying and its impacts here as it might
easily lead to unwelcome results, so it's worth pointing out.
2023-12-12 16:45:00 +01:00
Wojtek Mach 9675e2285a elixir.bat: Quote file paths (#13172) 2023-12-12 14:32:06 +01:00
José Valim a09ddbb0a6 Fix typo in CHANGELOG 2023-12-12 12:43:34 +01:00
José Valim 81a12b7774 Release v1.16.0-rc.1 2023-12-12 12:36:20 +01:00
José Valim 88ba7766e0 Use Macro.Env in more warnings 2023-12-11 11:13:39 +01:00
José Valim 6475917b3f Use Macro.Env to record attribute warnings
Closes #13162.
Closes #13164.
2023-12-11 07:34:16 +01:00
José Valim 5b6e04cb4d Clean up failed deletion warning 2023-12-10 09:03:54 +01:00
Daven 2bfb14751f Add warning when deps clean fails (#13161) 2023-12-10 09:03:54 +01:00
José Valim b1998960bb Disable compiler optimizations only in module body 2023-12-09 20:49:40 +11:00
José Valim c4dcbf379f Pass original exception down to details in diagnostic, closes #13142 2023-12-09 20:49:40 +11:00
Himanshu 3b5bd39743 Update case-cond-and-if.md (#13158) 2023-12-08 21:58:51 +11:00
Wojtek Mach 4e4cde1118 Update Windows installer to write Elixir install root to registry (#13157)
We don't need this right now but it could be useful in the future, if
anything to detect if Elixir was installed using this installer.

Demo:

    iex> {:ok, r} = :win32reg.open([:read])
    iex> :win32reg.change_key(r, ~c"\\hklm\\software\\wow6432node\\elixir\\elixir")
    iex> :win32reg.value(r, ~c"installroot")
    {:ok, ~c"C:\\Program Files\\Elixir"}
2023-12-07 10:28:23 +11:00
Wojtek Mach e1db6d8831 Update Windows installer to register in Add/Remove Programs (#13156) 2023-12-07 08:44:39 +11:00
José Valim db8a1cdc7d Revert "Consider surround context until end whenever possible"
This reverts commit a65dae971f.
2023-12-04 22:34:56 +10:00
José Valim d25aacb207 Simplify offset handling in TokenMissingError 2023-12-04 22:08:53 +10:00
Vinícius Müller 106539b5d2 Improve unclosed delimiter messages (#13123) 2023-12-04 22:08:53 +10:00
José Valim d716bc2703 Include both priv and include in releases, closes #13145 2023-11-25 10:34:58 +08:00
José Valim aa0dcb9a70 Fix prying functions with only literals, closes #13133 2023-11-23 22:33:39 +08:00
Zeke Douandc4710n 01366ef526 Add Logger.levels/0 (#13136)
Co-authored-by: c4710n <c4710n@users.noreply.github.com>
2023-11-23 22:09:09 +08:00
Andrea Leopardi 6145599638 Add t/0 types to remaining ExUnit exceptions (#13139) 2023-11-23 12:17:02 +01:00
Andrea Leopardi abd4f54d78 Fix typo in Logger docs 2023-11-23 12:11:07 +01:00
Andrea Leopardi adff7f6d34 Add t/0 types for some ExUnit exceptions (#13134) 2023-11-23 17:15:59 +08:00
Andrea Leopardi 2364f04991 Add callback docs to ExUnit.Formatter (#13135) 2023-11-23 17:15:59 +08:00
Łukasz Samson 46a5c54844 Properly escape \ in Path.wildcard docs (#13137) 2023-11-23 17:15:59 +08:00
Andrea Leopardi 2caacae2e4 Add some specs and types to ExUnit.Formatter (#13130) 2023-11-23 17:15:59 +08:00
José Valim 1ff5e0c88b Improve Logger docs, closes #13119 2023-11-22 09:24:37 +08:00
José Valim c150876e5f Remove warning on non-ambiguous nullary remote call 2023-11-22 08:55:00 +08:00
Wojtek Mach 758da1e311 Update Mix.Task.preferred_cli_env/1 docs (#13114) 2023-11-16 14:00:50 +01:00
Wojtek Mach 250b48aa3b Update Mix.Config mentions (#13115)
ExDoc main emitted these warnings on Elixir main:

```
    warning: documentation references module "Mix.Config" but it is hidden
    │
 49 │   `Mix.Config`, which was specific to Mix and has been deprecated.
    │   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    │
    └─ lib/elixir/lib/config.ex:49: Config (module)

    warning: documentation references module "Mix.Config" but it is hidden
    │
 51 │   You can leverage `Config` instead of `Mix.Config` in three steps. The first
    │   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    │
    └─ lib/elixir/lib/config.ex:51: Config (module)
```
2023-11-16 14:00:50 +01:00
Jean Klingler 0a9ee79fbb Formatter keeps quotes in atom keys (#13108) 2023-11-15 14:03:28 +01:00
Jean Klingler 58b8b93ee6 Auto infer size of matched variable in bitstrings (#13106) 2023-11-15 14:03:28 +01:00
Wojtek Mach aef8673666 Preserve column when translating typespecs (#13101) 2023-11-14 09:59:01 +01:00
José Valim 901ec18aa7 Handle error in Macro.to_string/1, closes #13102 2023-11-14 00:53:00 +01:00
José Valim 56b62b64cb Consider start line in MismatchedDelimiterError 2023-11-13 12:28:48 +01:00
Artem Solomatin abbf9061ea Fix links references (#13099) 2023-11-12 10:07:26 +01:00
José Valim 8f28bd9c3f Fix GenServer cheatsheet link
Closes #13098.
2023-11-11 21:55:42 +01:00
Christopher Keele 68782004a7 Produce better error messages for non-binary mix git deps refspecs. (#13088)
When using git dependencies, a branch/ref/tag specifier
is passed verbatim to `System.cmd/3`. This can lead to
intimidating error messages when they are not provided
as a binary (for instance, an atom like `tag: :stable`):

```
** (ArgumentError) all arguments for System.cmd/3 must be binaries
    (elixir 1.15.6) lib/system.ex:1083: System.cmd/3
    (mix 1.15.6) lib/mix/scm/git.ex:287: Mix.SCM.Git.git!/2
```

This PR adds a check during git opts verification time to provide
better feedback.
2023-11-11 09:20:21 +01:00
José Valim 0a146d10cc Fix link, closes #13095 2023-11-11 09:00:07 +01:00
José Valim beaa33d088 Handle nil values in IO.warn 2023-11-10 12:53:47 +01:00
José Valim 2a94668e6e Let's not deprecate ...foo as the API may be useful
for the type system in the future.

This reverts commit f97d8585e8.
2023-11-10 12:36:38 +01:00
Cameron Duley 56e6494b36 Use in/2 in String.replace_invalid/2 guards (#13093) 2023-11-10 09:09:29 +01:00
Cameron Duley 033a177078 Fix String.replace_invalid/2 perf regressions (#13090) 2023-11-10 09:09:29 +01:00
Minh Daoandminhqdao b9fffa3b41 Fix typo in getting-started guide (#13085)
Co-authored-by: minhqdao <hello@minhdao.de>
2023-11-07 23:05:51 +01:00
Juan Barrios 7be85838fa Update float.ex description of ceil/2 and floor/2 (#13084) 2023-11-07 20:11:24 +01:00
Jacob Swanner 37f8832229 Fix Enum cheatsheet for drop/2 and take/2 with negative index (#13080) 2023-11-07 10:51:31 +01:00
José Valim 7ebf5c3032 Add :emit_warnings to Code.string_to_quoted 2023-11-06 16:31:05 +01:00
José Valim 7d4d42097d Restore code paths in archive.install/escript.install
Closes #13079.
2023-11-06 15:56:05 +01:00
Lucas Francisco da Matta Vegi 33e1b570bf Additional remarks for maintaining research history (#13078)
Similar to what we had already done with other anti-patterns that changed names
2023-11-06 15:56:05 +01:00
José Valim 8920cc1432 Fix case clause error on tokenizer 2023-11-06 13:29:19 +01:00
Erik André Jakobsen 9b480f985b [docs] Clean up sigils intro (#13077) 2023-11-06 13:29:19 +01:00
Łukasz Samson daab3f80a0 Fix crashes when :beam_lib.info(beam) returns error (#13075) 2023-11-05 20:15:33 +01:00
Tony Dang ec8782a486 Fix typo in "Getting Started - Enumerables and Streams" docs (#13073) 2023-11-04 11:14:48 +01:00
Ioannis Kyriazis 6745f775b2 is -> us (#13072) 2023-11-03 23:23:56 +01:00
Łukasz Samson 7923e4f383 Elixir 1.14.5 supports Erlang/OTP 26 (#13071) 2023-11-03 11:54:02 +01:00
Michał Łępicki d280846843 Fix typo: an dread -> and read (#13069) 2023-11-03 08:08:23 +01:00
Rich Morin 178b654a1b Fix typo (#13068) 2023-11-02 21:04:21 +01:00
José Valim 3a516a3f2b Docs to new options and functions 2023-11-02 19:32:25 +01:00
Cameron Duley d5316d558f Add String.replace_invalid/2 (#13067) 2023-11-02 19:32:25 +01:00
Jonatan Kłosko a7216decad Add offset option to File.stream! (#13063) 2023-11-02 17:58:22 +01:00
Michał Łępicki 8d545610d2 Fix Path.absname/2 spec (#13065) 2023-11-02 13:08:52 +01:00
José Valim 23c27b629d Warn if both :applications and :extra_applications are used 2023-11-02 10:19:11 +01:00
Panagiotis Nezis 0220d81513 Support --sparse in archive.install and escript.install (#13059) 2023-11-02 08:57:51 +01:00
José Valim aabe5dad76 Do not use Erlang/OTP 26.1 on CI (#13062)
It has a bug when looking up mismatched module names.
2023-11-02 08:30:01 +01:00
Łukasz Samson 741757f2a7 Lazily evaluate File.cwd! in Path.expand and Path.absname (#13061)
do not crash with File.Error with already absolute paths if File.cwd returns error or nil
2023-11-02 08:11:44 +01:00
José Valim f5e543576e Use explicit/implicit vs manual/automatic 2023-11-01 16:33:17 +01:00
José Valim e6b641a075 Fix typo on docs 2023-11-01 16:11:26 +01:00
rktjmp 3822609621 Restore GenServer introduction mermaid graph (#13058)
Restores graph removed in f5a61d1, with correct request -> reply arrow
ordering.
2023-11-01 16:11:26 +01:00
Panagiotis Nezis 0f65cb065a Additional remarks for application config anti-pattern for Mix tasks (#13057) 2023-11-01 12:42:56 +01:00
José Valim ae19236e08 Bring behaviour section from website 2023-11-01 11:17:59 +01:00
José Valim f422b77aaa Update docs 2023-11-01 10:34:59 +01:00
José Valim 43b3e94506 Add more examples to unrelated clauses 2023-10-31 20:57:30 +01:00
José Valim 2d86cb0027 Handle warnings from unquote functions 2023-10-31 12:00:04 +01:00
José Valim 792d4cc631 Release v1.16.0-rc.0 2023-10-31 09:16:09 +01:00
José Valim f8016bca48 Describe them as potential anti-patterns 2023-10-31 08:52:14 +01:00
José Valim 82818f7126 Improve complex extraction example 2023-10-31 07:49:48 +01:00
José Valim 5c8f9aac64 Fix getting started links 2023-10-30 23:40:15 +01:00
José Valim 1a8bad1bf2 Streamline unrelated introduction 2023-10-30 20:10:53 +01:00
José Valim 4b795871b6 Improve examples and docs 2023-10-30 15:50:53 +01:00
José Valim 3036401c7c Clarify best practices and update anti-patterns list 2023-10-30 15:12:41 +01:00
José Valim 8bbba572de Describe pattern matching as simpler 2023-10-30 09:18:25 +01:00
José Valim 968b51319e Clarify scope of anti-patterns 2023-10-30 08:11:25 +01:00
José Valim 900f25f832 Branch out v1.16 2023-10-29 12:30:32 +01:00
339 changed files with 10067 additions and 15311 deletions
+5 -5
View File
@@ -22,13 +22,13 @@ jobs:
max-parallel: 1
matrix:
include:
- otp: 25
otp_version: "25.3"
- otp: 24
otp_version: '24.3'
upload_generic_version: upload_generic_version
- otp: 25
otp_version: '25.3'
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
+1 -2
View File
@@ -27,8 +27,7 @@ jobs:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@v16.0.0
uses: DavidAnson/markdownlint-cli2-action@v13.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+4 -3
View File
@@ -24,11 +24,12 @@ jobs:
fail-fast: false
matrix:
include:
- otp_version: "27.0"
otp_latest: true
- otp_version: "26.0"
otp_latest: true
- otp_version: "25.3"
- otp_version: "25.0"
- otp_version: "24.3"
- otp_version: "24.0"
- otp_version: master
development: true
- otp_version: maint
@@ -76,7 +77,7 @@ jobs:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["25.3", "26.0"]
otp_version: ["24", "25", "26.0"]
runs-on: windows-2019
steps:
- name: Configure Git
+4 -4
View File
@@ -33,12 +33,12 @@ jobs:
fail-fast: true
matrix:
include:
- otp: 24
otp_version: '24.3'
- otp: 25
otp_version: "25.3"
otp_version: '25.3'
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
-5
View File
@@ -17,19 +17,14 @@
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
+190 -161
View File
@@ -1,233 +1,262 @@
# Changelog for Elixir v1.17
# Changelog for Elixir v1.16
This release includes type inference of patterns to provide warnings for an initial set of constructs (binaries, maps, and atoms) within the same function. It also includes a new Duration data type to interact with Calendar types, support for Erlang/OTP 27, and many other improvements.
## Code snippets in diagnostics
## Warnings from gradual set-theoretic types
Elixir v1.15 introduced a new compiler diagnostic format and the ability to print multiple error diagnostics per compilation (in addition to multiple warnings).
This release introduces gradual set-theoretic types to infer types from patterns and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391).
With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened:
At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language:
* `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()` - these types are indivisible. This means both `1` and `13` get the same `integer()` type.
* `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types.
* `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key.
* `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them.
We focused on atoms and maps on this initial release as they are respectively the simplest and the most complex types representations, so we can stress the performance of the type system and quality of error messages. Modelling these types will also provide the most immediate benefits to Elixir developers. Assuming there is a variable named `user`, holding a `%User{}` struct with an `address` field, Elixir v1.17 will emit the following warnings at compile-time:
* Pattern matching against a map or a struct that does not have the given key, such as `%{adress: ...} = user` (notice `address` vs `adress`)
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0`
* Attempting to invoke to call an anonymous function without an actual function, such as `user.()`
* Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]`
* Performing structural comparisons between non-overlapping types, such as `integer >= string`
* Building and pattern matching on binaries without the relevant specifiers, such as `<<name>>` (this warns because by default it expects an integer, it should have been `<<name::binary>>` instead)
* Attempting to rescue an undefined exception or a struct that is not an exception
* Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
## Erlang/OTP support
This release adds support for Erlang/OTP 27 and drops support for Erlang/OTP 24. We recommend Elixir developers to migrate to Erlang/OTP 26 or later, especially on Windows. Support for WERL (a graphical user interface for the Erlang terminal on Windows) will be removed in Elixir v1.18.
## Adding `Duration` and `shift/2` functions
Elixir introduces the `Duration` data type and APIs to shift dates, times, and date times by a given duration, considering different calendars and time zones.
```elixir
iex> Date.shift(~D[2016-01-31], month: 2)
~D[2016-03-31]
```
** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17:
error: syntax error before: '*'
│
1 │ [1, 2, 3, 4, 5, *]
│ ^
│
└─ lib/my_app.ex:1:17
```
Note the operation is called `shift` (instead of `add`) since working with durations does not obey properties such as associativity. For instance, adding one month and then one month does not give the same result as adding two months:
For mismatched delimiters, it now shows both delimiters:
```elixir
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
```
** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18:
error: unexpected token: )
│
1 │ [1, 2, 3, 4, 5, 6)
│ │ └ mismatched closing delimiter (expected "]")
│ └ unclosed delimiter
│
└─ lib/my_app.ex:1:18
```
Still, durations are essential for building intervals, recurring events, and modelling scheduling complexities found in the world around us. For `DateTime`s, Elixir will correctly deal with time zone changes (such as Daylight Saving Time), but provisions are also available in case you want to surface conflicts (for example, you shifted to a wall clock that does not exist, because the clock has been moved forward by one hour). See `DateTime.shift/2` for examples.
For unclosed delimiters, it now shows where the unclosed delimiter starts:
Finally, a new `Kernel.to_timeout/1` function has been added, which helps developers normalize durations and integers to a timeout used by Process APIs. For example, to send a message after one hour, one can now write:
```elixir
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
```
** (TokenMissingError) token missing on lib/my_app:8:23:
error: missing terminator: )
│
1 │ my_numbers = (1, 2, 3, 4, 5, 6
│ └ unclosed delimiter
...
8 │ IO.inspect(my_numbers)
│ └ missing closing delimiter (expected ")")
│
└─ lib/my_app:8:23
```
## v1.17.3 (2024-09-18)
Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables:
```
error: undefined variable "unknown_var"
│
5 │ a - unknown_var
│ ^^^^^^^^^^^
│
└─ lib/sample.ex:5:9: Sample.foo/1
```
Otherwise the whole line is underlined:
```
error: function names should start with lowercase characters or underscore, invalid name CamelCase
│
3 │ def CamelCase do
│ ^^^^^^^^^^^^^^^^
│
└─ lib/sample.ex:3
```
A huge thank you to Vinícius Müller for working on the new diagnostics.
## Revamped documentation
Elixir's Getting Started guides have been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references.
We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers examples of potential anti-patterns, with context and examples on how to improve their codebases.
Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue.
Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs.
## v1.16.3 (2024-05-21)
### 1. Bug fixes
#### Elixir
* [Duration] Fix parsing of fractional durations with non-positive seconds
* [Kernel] Do not attempt to group module warnings when they have a large context
#### IEx
* [IEx.Helpers] Properly reconsolidate protocols on `recompile`
#### Mix
* [mix compile.elixir] Do not verify modules twice
* [mix xref] Respect the `--label` option on stats and cycles
## v1.17.2 (2024-07-06)
### 1. Bug fixes
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
* [Code.Formatter] Add brackets around keyword lists when formatting the left-hand side of `when`
* [Kernel] Only infer size in pinned variable in binary strings when needed
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
#### Logger
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
* [Logger] Ensure translators are persisted across logger restarts
#### Mix
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
* [mix compile] Ensure compile paths are accessible during compilation
## v1.17.1 (2024-06-18)
## v1.16.2 (2024-03-10)
### 1. Enhancements
#### Elixir
* [Code] Emit `:defmodule` tracing event on module definition
#### Mix
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
* [Mix] Add `Mix.install_project_dir/0`
* [Mix] Add environment variable for reusing `Mix.install/2` installation
* [Mix.SCM] Add `Mix.SCM.delete/1`
### 2. Bug fixes
#### Elixir
* [Code] Fix charlist formatting issue when a single-quoted charlist escapes a double-quote character
* [Path] Fix regression on how `Path.relative_to/2` dealt with "." as input
#### IEx
* [IEx.Helpers] Discard mermaid fenced blocks from ansi docs
#### ExUnit
* [ExUnit] Properly compared pinned values when building diffs
## v1.16.1 (2024-01-31)
### 1. Bug fixes
#### Elixir
* [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand
* [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation
* [Kernel.ParallelCompiler] Always log errors at the end of compilation
* [String] Fix `String.capitalize/1` with a single codepoint
#### IEx
* [IEx] Fix autocompletion of function signatures on Erlang/OTP 26
* [IEx] Do not assume `$HOME` is set
#### Mix
* [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix
* [mix test] Properly resolve relative paths when running tests from individual files
* [mix test] Properly resolve Windows paths when running tests from individual files
## v1.16.0 (2023-12-22)
### 1. Enhancements
#### EEx
* [EEx] Do not warn for assignment with blocks in EEx
* [EEx] Include relative file information in diagnostics
#### Elixir
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
#### ExUnit
* [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
## v1.17.0 (2024-06-12)
### 1. Enhancements
#### Elixir
* [Access] Add `Access.find/1` that mirrors `Enum.find/2`
* [Code] Support cursor inside fn/rescue/catch/else/after inside `Code.Fragment.container_cursor_to_quoted/2`
* [Date] Add `Date.shift/2` to shift dates with duration and calendar-specific semantics
* [Date] Allow `Date` to accept years outside of `-9999..9999` range
* [DateTime] Add `DateTime.shift/2` to shift datetimes with duration and calendar-specific semantics
* [Duration] Add a new `Duration` data type
* [GenServer] Add `c:GenServer.format_status/1` callback
* [Kernel] Add `Kernel.get_in/1` with safe nil-handling for access and structs
* [Kernel] Add `Kernel.is_non_struct_map/1` guard
* [Kernel] Add `Kernel.to_timeout/1`
* [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code
* [Kernel] Support integers in uppercase sigils
* [Keyword] Add `Keyword.intersect/2-3` to mirror the `Map` API
* [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages
* [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics
* [Process] Add `Process.set_label/1`
* [String] Add `String.byte_slice/3` to slice a string to a maximum number of bytes while keeping it UTF-8 encoded
* [System] Support `use_stdio: false` in `System.cmd/3` and `System.shell/2`
* [Time] Add `Time.shift/2` to shift times with duration and calendar-specific semantics
#### ExUnit
* [ExUnit] Propagate the test process itself as a caller in `start_supervised`
* [ExUnit] Include max cases in ExUnit reports
#### IEx
* [IEx.Helpers] Warns if `recompile` was called and the current working directory changed
* [IEx.Helpers] Add `c/0` as an alias to `continue/0`
* [IEx.Pry] Add `IEx.Pry.annotate_quoted/3` to annotate a quoted expression with pry breakpoints
* [Code] Add `:emit_warnings` for `Code.string_to_quoted/2`
* [Code] Automatically include columns in parsing options
* [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions
* [Code.Fragment] Handle anonymous calls in fragments
* [Code.Formatter] Trim trailing whitespace on heredocs with `\r\n`
* [File] Add `:offset` option to `File.stream!/2`
* [Kernel] Auto infer size of matched variable in bitstrings
* [Kernel] Preserve column information when translating typespecs
* [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError`
* [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel
* [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias
* [Macro] Add `Macro.compile_apply/4`
* [Module] Add support for `@nifs` annotation from Erlang/OTP 25
* [Module] Add support for missing `@dialyzer` configuration
* [String] Update to Unicode 15.1.0
* [String] Add `String.replace_invalid/2`
* [Task] Add `:limit` option to `Task.yield_many/2`
#### Logger
* [Logger] Format `:gen_statem` reports using Elixir data structures
* [Logger] Include process label in logger events
* [Logger] Add `Logger.levels/0`
#### Mix
* [mix deps] Add `:depth` option to `Mix.SCM.Git`, thus supporting shallow clones of Git dependencies
* [mix deps] Warn if `:optional` is used in combination with `:in_umbrella`
* [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped
* [mix deps.tree] Add `--umbrella-only` to `mix deps.tree`
* [mix profile.tprof] Add a new profiler, available on Erlang/OTP 27+, which can measure count, time, and heap usage
* [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run
* [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix test] Generate cover HTML files in parallel
* [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks
* [mix archive.install] Support `--sparse` option
* [mix compile.app] Warn if both `:applications` and `:extra_applications` are used
* [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
* [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
* [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
* [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata
* [mix escript.install] Support `--sparse` option
* [mix release] Include `include/` directory in releases
* [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`
### 2. Bug fixes
#### Elixir
* [bin/elixir.bat] Improve handling of quotes and exclamation marks in flags
* [Code] Address a bug where AST nodes for `(a -> b)` were not wrapped as part of the literal encoder
* [Kernel] Resolve inconsistencies of how `..` and `...` are handled at the AST level
* [Kernel] Fix parsing precedence of ambiguous operators followed by containers
* [Kernel] Do not expand code in `quote bind_quoted: ...` twice
* [Kernel] Respect `:line` property when `:file` is given as option to `quote`
* [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta
* [Kernel] Avoid double tracing events when capturing a function
* [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
* [Code] Keep quotes for atom keys in formatter
* [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->`
* [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`)
* [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded)
* [Kernel] Ensure `dbg` module is a compile-time dependency
* [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused
* [Kernel] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs)
* [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition
* [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks
* [Macro] Address exception on `Macro.to_string/1` for certain ASTs
* [Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1`
* [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd`
#### ExUnit
* [ExUnit] Raise on incorrectly dedented doctests
#### IEx
* [IEx.Helpers] Update the history size whenever history is pruned
* [IEx.Pry] Fix prying functions with only literals in their body
#### Mix
* [mix deps] Fix error message for diverged SCM definition in sibling
* [mix archive.install] Restore code paths after `mix archive.install`
* [mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change
* [mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero)
* [mix escript.install] Restore code paths after `mix escript.install`
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+
* [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)`
* [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2`
* [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2`
#### Mix
* [mix profile.cprof] Deprecated in favor of the new `mix profile.tprof`
* [mix profile.eprof] Deprecated in favor of the new `mix profile.tprof`
* [mix compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic`
### 4. Hard deprecations
#### Elixir
* [IO] Passing `:all` to `IO.read/2` and `IO.binread/2` is deprecated, pass `:eof` instead
* [Kernel] Single-quote charlists are deprecated, use `~c` instead
* [Kernel] Deprecate escaping closing delimiter in uppercase sigils
* [Range] `left..right` without explicit steps inside patterns and guards is deprecated, write `left..right//step` instead
* [Range] Decreasing ranges, such as `10..1` without an explicit step is deprecated, write `10..1//-1` instead
* [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead
* [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead
* [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils
* [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead
#### ExUnit
* [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons
* [ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead
## v1.16
#### Mix
The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md).
* [mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler
* [mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler
## v1.15
The CHANGELOG for v1.15 releases can be found [in the v1.15 branch](https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md).
+4 -3
View File
@@ -27,9 +27,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 25)])' -s erlang halt | grep -q '^true'; \
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 25.0 is required to build Elixir"; \
echo "At least Erlang/OTP 24.0 is required to build Elixir"; \
exit 1; \
fi
endef
@@ -177,8 +177,9 @@ clean_residual_files:
#==> Documentation tasks
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
+33 -37
View File
@@ -1,11 +1,7 @@
<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture>
</h1>
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
@@ -16,7 +12,7 @@ For more about Elixir, installation and documentation,
## Policies
New releases are announced in the [announcement mailing list][8].
You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com>
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
and replying to the confirmation email.
All security releases [will be tagged with `[security]`][10]. For more
@@ -29,28 +25,28 @@ All interactions in our official communication channels follow our
For reporting bugs, [visit our issue tracker][2] and follow the steps
for reporting a new issue. **Please disclose security vulnerabilities
privately at <elixir-security@googlegroups.com>**.
privately at elixir-security@googlegroups.com**.
## Issues tracker management
All currently open bugs related to the Elixir repository are listed
in the issues tracker. The Elixir team uses the issues tracker to focus
on *actionable items*, including planned enhancements in the short and
medium term. We also do our best to label entries for clarity and to ease
on *actionable items*, including planned enhancements in the short- and
medium-term. We also do our best to label entries for clarity and to ease
collaboration.
Our *actionable item policy* has some important consequences, such as:
* Proposing new features as well as requests for support, help, and
* Proposing new features as well as request for support, help, and
guidance must be done in their own spaces, detailed next.
* Issues we have identified to be outside of Elixir's scope,
such as an upstream bug, will be closed (and requested to be moved
* Issues where we have identified to be outside of Elixir scope,
such as a bug upstream, will be closed (and requested to be moved
elsewhere if appropriate).
* We actively close unrelated and non-actionable issues to keep the
issues tracker tidy. We may get things wrong from time to
time and will gladly revisit issues, reopening when necessary.
issues tracker tidy. However, we may get things wrong from time to
time, so we are glad to revisit issues and reopen if necessary.
Keep the tone positive and be kind! For more information, see the
[Code of Conduct][1].
@@ -76,7 +72,7 @@ in the next release are then "closed" and added to the [changelog][7].
### Discussions, support, and help
For general discussions, support, and help, please use the community
For general discussions, support, and help, please use many of the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available
to help you.
@@ -107,29 +103,29 @@ Additionally, you may choose to run the test suite with `make clean test`.
## Contributing
We invite contributions to Elixir. To contribute, there are a few
We welcome everyone to contribute to Elixir. To do so, there are a few
things you need to know about the code. First, Elixir code is divided
by each application inside the `lib` folder:
in applications inside the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `elixir` - Elixir's kernel and standard library
* `eex` - EEx is the template engine that allows you to embed Elixir
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test`. You can
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
You can run all tests in the root directory with `make test` and you can
also run tests for a specific framework `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are only changing one file, you can choose to compile and run tests
for that specific file for faster development cycles. For example, if you
If you are changing just one file, you can choose to compile and run tests only
for that particular file for fast development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
@@ -150,7 +146,7 @@ make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted, then run the full suite with
all files are properly formatted and then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
@@ -160,7 +156,7 @@ you can rebuild the language from scratch with:
make clean_elixir compile
```
Similarly, if you can not get Elixir to compile or the tests to pass after
Similarly, if you can't get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
@@ -170,9 +166,9 @@ With tests running and passing, you are ready to contribute to Elixir and
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
### Reviewing changes
@@ -180,7 +176,7 @@ Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
the repository. If any changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
@@ -200,8 +196,8 @@ a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
to be installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
+5 -7
View File
@@ -8,17 +8,15 @@
3. Update "Compatibility and Deprecations" if a new OTP version is supported
4. Commit changes above with title "Release vVERSION" and push it
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it
5. Once GitHub actions completes, generate a new tag, and push it
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
6. Wait until GitHub Actions publish artifacts to the draft release
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
## Creating a new vMAJOR.MINOR branch (after first rc)
### In the new branch
+2 -2
View File
@@ -6,11 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.17 | Bug fixes and security patches
1.16 | Security patches only
1.16 | Bug fixes and security patches
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
1.12 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.17.3
1.16.3
+2 -3
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.17.3
ELIXIR_VERSION=1.16.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -218,7 +218,6 @@ SELF=$(readlink_f "$0")
SCRIPT_PATH=$(dirname "$SELF")
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
@@ -229,7 +228,7 @@ fi
ERTS_BIN=
ERTS_BIN="$ERTS_BIN"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@"
if [ -n "$RUN_ERL_PIPE" ]; then
ESCAPED=""
+59 -28
View File
@@ -1,7 +1,8 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
set ELIXIR_VERSION=1.17.3
set ELIXIR_VERSION=1.16.3
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
@@ -63,11 +64,13 @@ echo ** Standalone options can't be combined with other options.
goto end
:shortversion
echo %ELIXIR_VERSION%
echo !ELIXIR_VERSION!
goto end
:parseopts
setlocal enabledelayedexpansion
rem Parameters for Elixir
set parsElixir=
rem Parameters for Erlang
set parsErlang=
@@ -94,24 +97,48 @@ if "!par!"=="" (
)
shift
set par="!par:"=\"!"
if !endLoop! == 1 (
set parsElixir=!parsElixir! !par!
goto startloop
)
rem ******* EXECUTION OPTIONS **********************
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set useIEx=1 && goto startloop)
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && goto startloop)
rem ******* EVAL PARAMETERS ************************
if ""==!par:-e=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--eval=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--rpc-eval=! (
set "VAR=%~2"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
shift
shift
goto startloop
)
rem ******* ELIXIR PARAMETERS **********************
if ""==!par:-e=! (shift && goto startloop)
if ""==!par:--eval=! (shift && goto startloop)
if ""==!par:--rpc-eval=! (shift && shift && goto startloop)
if ""==!par:-r=! (shift && goto startloop)
if ""==!par:-pr=! (shift && goto startloop)
if ""==!par:-pa=! (shift && goto startloop)
if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %~1" && shift && goto startloop)
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %~1" && shift && goto startloop)
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %~1" && shift && goto startloop)
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %~1" && shift && goto startloop)
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift && goto startloop)
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop)
if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop)
rem ******* ERLANG PARAMETERS **********************
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
@@ -125,30 +152,34 @@ if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1""
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
set endLoop=1
set parsElixir=!parsElixir! !par!
goto startloop
:run
setlocal disabledelayedexpansion
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true %beforeExtra%
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
)
if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
if defined useIEx (
set beforeExtra=-s elixir start_iex !beforeExtra!
) else (
set beforeExtra=-s elixir start_cli !beforeExtra!
)
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra!
if defined ELIXIR_CLI_DRY_RUN (
if defined useWerl (
echo start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
) else (
if defined useWerl (
start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
)
exit /B %ERRORLEVEL%
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
set argc=0
for %%A in (%*) do (
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
+1 -1
View File
@@ -1,2 +1,2 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
+2 -2
View File
@@ -317,8 +317,8 @@ defmodule EEx do
## Examples
iex> EEx.tokenize(~c"foo", line: 1, column: 1)
{:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
iex> EEx.tokenize('foo', line: 1, column: 1)
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
## Result
+11 -11
View File
@@ -71,10 +71,10 @@ defmodule EEx.Compiler do
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} ->
{:ok, _line, _column, _warnings, tokens} ->
# We ignore warnings because the code will be tokenized
# again later with the right line+column info
token_key(rev_tokens, expr)
token_key(tokens, expr)
{:error, _, _, _, _} ->
{:expr, expr}
@@ -164,8 +164,8 @@ defmodule EEx.Compiler do
end
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
defp token_key(tokens, expr) do
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
@@ -340,6 +340,13 @@ defmodule EEx.Compiler do
scope,
state
) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line
start_column = column(meta.column, mark)
@@ -352,13 +359,6 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
end
+9
View File
@@ -43,6 +43,15 @@ defmodule EEx.SmartEngineTest do
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected
-10
View File
@@ -543,16 +543,6 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do
warning =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
+34 -123
View File
@@ -35,74 +35,61 @@ defmodule Access do
iex> nil[:a]
nil
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
to allow values to be set in nested data structures:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"][:age], 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
## Maps and structs
While the access syntax is allowed in maps via `map[key]`,
if your map is made of predefined atom keys, you should prefer
to access those atom keys with `map.key` instead of `map[key]`,
as `map.key` will raise if the key is missing (which is not
supposed to happen if the keys are predefined) or if `map` is
`nil`.
supposed to happen if the keys are predefined).
Similarly, since structs are maps and structs have predefined
keys, they only allow the `struct.key` syntax and they do not
allow the `struct[key]` access syntax.
allow the `struct[key]` access syntax. `Access.key/1` can also
be used to construct dynamic access to structs and maps.
In other words, the `map[key]` syntax is loose, returning `nil`
for missing keys, while the `map.key` syntax is strict, raising
for both nil values and missing keys.
In a nutshell, when using `put_in/2` and friends:
To bridge this gap, Elixir provides the `get_in/1` and `get_in/2`
functions, which are capable of traversing nested data structures,
even in the presence of `nil`s:
put_in(struct_or_map.key, :value)
put_in(keyword_or_map[:key], :value)
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users["john"].age)
27
iex> get_in(users["unknown"].age)
nil
When using `put_in/3` and friends:
Notice how, even if no user was found, `get_in/1` returned `nil`.
Outside of `get_in/1`, trying to access the field `.age` on `nil`
would raise.
put_in(struct_or_map, [Access.key!(:key)], :value)
put_in(keyword_or_map, [:key], :value)
The `get_in/2` function takes one step further by allowing
different accessors to be mixed in. For example, given a user
map with the `:name` and `:languages` keys, here is how to
access the name of all programming languages:
This covers the dual nature of maps in Elixir, as they can be
either for structured data or as a key-value store. See the `Map`
module for more information.
iex> languages = [
...> %{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}
...> ]
iex> user = %{name: "john", languages: languages}
iex> get_in(user, [:languages, Access.all(), :name])
["elixir", "c"]
## Nested data structures
This module provides convenience functions for traversing other
structures, like tuples and lists. As we will see next, they can
even be used to update nested data structures.
Both key-based access syntaxes can be used with the nested update
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3`.
If you want to learn more about the dual nature of maps in Elixir,
as they can be either for structured data or as a key-value store,
see the `Map` module.
## Updating nested data structures
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2`, `Kernel.get_and_update_in/2`, and `Kernel.pop_in/1`
macros to further manipulate values in nested data structures:
For example, to update a map inside another map:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"].age, 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
As shown in the previous section, you can also use the
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3` functions to provide nested
custom accessors. For instance, given a user map with the
`:name` and `:languages` keys, here is how to deeply traverse
the map and convert all language names to uppercase:
This module provides convenience functions for traversing other
structures, like tuples and lists. These functions can be used
in all the `Access`-related functions and macros in `Kernel`.
For instance, given a user map with the `:name` and `:languages`
keys, here is how to deeply traverse the map and convert all
language names to uppercase:
iex> languages = [
...> %{name: "elixir", type: :functional},
@@ -565,7 +552,7 @@ defmodule Access do
{"john", %{user: %{name: "JOHN"}}}
However, it is not possible to remove fields using the dot notation,
as it is implied those fields must also be present. In any case,
as it is implified those fields must also be present. In any case,
`Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically.
@@ -1026,80 +1013,4 @@ defmodule Access do
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)}
end
@doc ~S"""
Returns a function that accesses the first element of a list that matches the provided predicate.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
## Examples
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
"francine"
iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
`find/1` can also be used to pop the first found element out of a list or
a key inside of a list:
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
nil
iex> get_and_update_in(list, [Access.find(&(&1.salary >= 50)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
** (RuntimeError) Access.find/1 expected a list, got: %{}
"""
@doc since: "1.17.0"
@spec find((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: term)
def find(predicate) when is_function(predicate, 1) do
fn op, data, next -> find(op, data, predicate, next) end
end
defp find(:get, data, predicate, next) when is_list(data) do
data |> Enum.find(predicate) |> next.()
end
defp find(:get_and_update, data, predicate, next) when is_list(data) do
get_and_update_find(data, [], predicate, next)
end
defp find(_op, data, _predicate, _next) do
raise "Access.find/1 expected a list, got: #{inspect(data)}"
end
defp get_and_update_find([], updates, _predicate, _next) do
{nil, :lists.reverse(updates)}
end
defp get_and_update_find([head | rest], updates, predicate, next) do
if predicate.(head) do
case next.(head) do
{get, update} -> {get, :lists.reverse([update | updates], rest)}
:pop -> {head, :lists.reverse(updates, rest)}
end
else
get_and_update_find(rest, [head | updates], predicate, next)
end
end
end
+11 -28
View File
@@ -13,7 +13,7 @@ defmodule Application do
Developers typically interact with the application environment and its
callback module. Therefore those will be the topics we will cover first
before jumping into details about the application resource file and life cycle.
before jumping into details about the application resource file and life-cycle.
## The application environment
@@ -258,8 +258,8 @@ defmodule Application do
Application.stop(:ex_unit)
#=> :ok
Stopping an application without a callback module defined, is in practice a
no-op, except for some system tracing.
Stopping an application without a callback module is defined, but except for
some system tracing, it is in practice a no-op.
Stopping an application with a callback module has three steps:
@@ -277,7 +277,7 @@ defmodule Application do
invoked only after termination of the whole supervision tree.
Shutting down a live system cleanly can be done by calling `System.stop/1`. It
will shut down every application in the reverse order they were started.
will shut down every application in the opposite order they had been started.
By default, a SIGTERM from the operating system will automatically translate to
`System.stop/0`. You can also have more explicit control over operating system
@@ -817,29 +817,16 @@ defmodule Application do
end
@doc """
Puts the environment for multiple applications at the same time.
Puts the environment for multiple apps at the same time.
The given config should not:
* have the same application listed more than once
* have the same key inside the same application listed more than once
If those conditions are not met, this function will raise.
This function receives the same options as `put_env/4`. Returns `:ok`.
## Examples
Application.put_all_env(
my_app: [
key: :value,
another_key: :another_value
],
another_app: [
key: :value
]
)
If those conditions are not met, it will raise.
It receives the same options as `put_env/4`. Returns `:ok`.
"""
@doc since: "1.9.0"
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
@@ -863,10 +850,7 @@ defmodule Application do
# TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env.
defp maybe_warn_on_app_env_key(app, key) do
message = fn ->
"passing non-atom as application env key is deprecated, got: #{inspect(key)}"
end
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
@@ -905,12 +889,11 @@ defmodule Application do
## Options
* `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
* `:type` - if the application should be started in `:permanent`,
`:temporary`, or `:transient`. See `t:restart_type/1` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`). This option requires
Erlang/OTP 26+.
or concurrently. This option requires Erlang/OTP 26+.
"""
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
+1 -1
View File
@@ -67,7 +67,7 @@ defmodule Atom do
## Examples
iex> Atom.to_charlist(:"An atom")
~c"An atom"
'An atom'
"""
@spec to_charlist(atom) :: charlist
+1 -1
View File
@@ -2,7 +2,7 @@ defmodule Bitwise do
@moduledoc """
A set of functions that perform calculations on bits.
All bitwise functions work only on integers, otherwise an
All bitwise functions work only on integers; otherwise an
`ArithmeticError` is raised. The functions `band/2`,
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
+3 -31
View File
@@ -70,14 +70,14 @@ defmodule Calendar do
@type zone_abbr :: String.t()
@typedoc """
The time zone UTC offset in ISO seconds for standard time.
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 ISO seconds (typically not zero in summer times).
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".
"""
@@ -338,34 +338,6 @@ defmodule Calendar do
@doc since: "1.15.0"
@callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """
Shifts date by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """
Shifts naive datetime by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
@doc """
Shifts time by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
# General Helpers
@doc """
@@ -733,7 +705,7 @@ defmodule Calendar do
# 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)
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
+8 -93
View File
@@ -40,7 +40,7 @@ defmodule Date do
## Using epochs
The `add/2`, `diff/2` and `shift/2` functions can be used for computing dates
The `add/2` and `diff/2` functions can be used for computing dates
or retrieving the number of days between instants. For example, if there
is an interest in computing the number of days from the Unix epoch
(1970-01-01):
@@ -51,9 +51,6 @@ defmodule Date do
iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
~D[2010-04-17]
Those functions are optimized to deal with common epochs, such
as the Unix Epoch above or the Gregorian Epoch (0000-01-01).
"""
@@ -690,8 +687,6 @@ defmodule Date do
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
## Examples
iex> Date.add(~D[2000-01-03], -2)
@@ -708,7 +703,12 @@ defmodule Date do
@spec add(Calendar.date(), integer()) :: t
def add(%{calendar: Calendar.ISO} = date, days) do
%{year: year, month: month, day: day} = date
{year, month, day} = Calendar.ISO.shift_days({year, month, day}, days)
{year, month, day} =
Calendar.ISO.date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> Calendar.ISO.date_from_iso_days()
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
@@ -757,81 +757,6 @@ defmodule Date do
end
end
@doc """
Shifts given `date` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months and then days:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
When shifting by month, days are rounded down to the nearest valid date.
Raises an `ArgumentError` when called with time scale units.
## Examples
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
iex> Date.shift(~D[2016-01-30], month: -1)
~D[2015-12-30]
iex> Date.shift(~D[2016-01-31], year: 4, day: 1)
~D[2020-02-01]
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
# leap years
iex> Date.shift(~D[2024-02-29], year: 1)
~D[2025-02-28]
iex> Date.shift(~D[2024-02-29], year: 4)
~D[2028-02-29]
# rounding down
iex> Date.shift(~D[2015-01-31], month: 1)
~D[2015-02-28]
"""
@doc since: "1.17.0"
@spec shift(Calendar.date(), Duration.t() | [unit_pair]) :: t
when unit_pair: {:year, integer} | {:month, integer} | {:week, integer} | {:day, integer}
def shift(%{calendar: calendar} = date, duration) do
%{year: year, month: month, day: day} = date
{year, month, day} = calendar.shift_date(year, month, day, __duration__!(duration))
%Date{calendar: calendar, year: year, month: month, day: day}
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({unit, _value})
when unit in [:hour, :minute, :second, :microsecond] do
raise ArgumentError, "unsupported unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({unit, _value}) when unit not in [:year, :month, :week, :day] do
raise ArgumentError, "unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc false
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
@@ -1158,18 +1083,8 @@ defmodule Date do
end
defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar == Calendar.ISO do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
end
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, #{inspect(calendar)})"
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+15 -169
View File
@@ -60,7 +60,7 @@ defmodule DateTime do
a date and time at a given time zone". To understand precisely
what we mean, let's see an example.
Imagine someone in Poland who wants to schedule a meeting with someone
Imagine someone in Poland wants to schedule a meeting with someone
in Brazil in the next year. The meeting will happen at 2:30 AM
in the Polish time zone. At what time will the meeting happen in
Brazil?
@@ -83,14 +83,14 @@ defmodule DateTime do
not a problem, because time zone rules do not change for past
events.
To make matters worse, it may be that 2:30 AM in Polish time
To make matters worse, it may be that the 2:30 AM in Polish time
does not actually even exist or it is ambiguous. If a certain
time zone observes "Daylight Saving Time", they will move their
clock forward once a year. When this happens, there is a whole
hour that does not exist. Then, when they move the clock back,
there is a certain hour that will happen twice. So if you want to
schedule a meeting when this shift back happens, you would need to
explicitly say which occurrence of 2:30 AM you mean: the one in
explicitly say which occurence of 2:30 AM you mean: the one in
"Summer Time", which occurs before the shift, or the one
in "Standard Time", which occurs after it. Applications that are
date and time sensitive need to take these scenarios into account
@@ -170,9 +170,6 @@ defmodule DateTime do
truncate the resulting datetime. This is available
since v1.15.0.
The default unit if none gets passed is `:native`,
which results on a default resolution of microseconds.
## Examples
iex> datetime = DateTime.utc_now()
@@ -897,7 +894,7 @@ defmodule DateTime do
-17412508655
"""
@spec to_unix(Calendar.datetime(), :native | System.time_unit()) :: integer
@spec to_unix(Calendar.datetime(), System.time_unit()) :: integer
def to_unix(datetime, unit \\ :second)
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
@@ -1497,11 +1494,6 @@ defmodule DateTime do
## Examples
iex> DateTime.diff(~U[2024-01-15 10:00:10Z], ~U[2024-01-15 10:00:00Z])
10
This function also considers timezone offsets:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
@@ -1613,8 +1605,6 @@ defmodule DateTime do
iex> result.microsecond
{21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
"""
@doc since: "1.8.0"
@spec add(
@@ -1643,28 +1633,30 @@ defmodule DateTime do
add(datetime, amount_to_add * 60, :second, time_zone_database)
end
def add(%{calendar: calendar} = datetime, amount_to_add, unit, time_zone_database)
when is_integer(amount_to_add) do
def add(datetime, amount_to_add, unit, time_zone_database) when is_integer(amount_to_add) do
%{
microsecond: {_, precision},
time_zone: time_zone,
utc_offset: utc_offset,
std_offset: std_offset
std_offset: std_offset,
calendar: calendar,
microsecond: {_, precision}
} = datetime
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
result =
datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
# Subtract total offset in order to get UTC and add the integer for the addition
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add - total_offset, ppd)
|> shift_zone_for_iso_days_utc(calendar, precision, datetime.time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
@@ -1677,152 +1669,6 @@ defmodule DateTime do
end
end
@doc """
Shifts given `datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
On the other hand, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen")
DateTime.shift(dt, hour: 1)
#=> #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
dt = DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles")
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2)
~U[2016-03-01 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4)
~U[2017-01-29 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: -25)
~U[2015-12-31 23:35:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4})
~U[2016-01-01 00:05:00.0005Z]
# leap years
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 1)
~U[2025-02-28 00:00:00Z]
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4)
~U[2028-02-29 00:00:00Z]
# rounding down
iex> DateTime.shift(~U[2015-01-31 00:00:00Z], month: 1)
~U[2015-02-28 00:00:00Z]
"""
@doc since: "1.17.0"
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
std_offset: std_offset,
utc_offset: utc_offset,
time_zone: time_zone
} = datetime
{year, month, day, hour, minute, second, {_, precision} = microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
result =
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
result_datetime
{:error, error} ->
raise ArgumentError,
"cannot shift #{inspect(datetime)} to #{inspect(duration)} (with time zone " <>
"database #{inspect(time_zone_database)}), reason: #{inspect(error)}"
end
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given datetime with the microsecond field truncated to the given
precision (`:microsecond`, `:millisecond` or `:second`).
-436
View File
@@ -1,436 +0,0 @@
defmodule Duration do
@moduledoc """
Struct and functions for handling durations.
A `Duration` struct represents a collection of time scale units,
allowing for manipulation and calculation of durations.
Date and time scale units are represented as integers, allowing for
both positive and negative values.
Microseconds are represented using a tuple `{microsecond, precision}`.
This ensures compatibility with other calendar types implementing time,
such as `Time`, `DateTime`, and `NaiveDateTime`.
## Shifting
The most common use of durations in Elixir's standard library is to
"shift" the calendar types.
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
In the example above, `Date.shift/2` automatically converts the units
into a `Duration` struct, although one can also be given directly:
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
It is important to note that shifting is not an arithmetic operation.
For example, adding `date + 1 month + 1 month` does not yield the same
result as `date + 2 months`. Let's see an example:
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
iex> ~D[2016-01-31] |> Date.shift(month: 2)
~D[2016-03-31]
As you can see above, the results differ, which explains why operations
with durations are called "shift" rather than "add". This happens because,
once we add one month to `2016-01-31`, we get `2016-02-29`. Then adding
one extra month gives us `2016-03-29` instead of `2016-03-31`.
In particular, when applying durations to `Calendar.ISO` types:
* larger units (such as years and months) are applied before
smaller ones (such as weeks, hours, days, and so on)
* units are collapsed into months (`:year` and `:month`),
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
and microseconds (`:microsecond`) before they are applied
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
Therefore, 4 weeks _are not_ equivalent to 1 month
* in case of non-existing dates, the results are rounded down to the
nearest valid date
As the `shift/2` functions are calendar aware, they are guaranteed to return
valid date/times, considering leap years as well as DST in applicable time zones.
## Intervals
Durations in Elixir can be combined with stream operations to build intervals.
For example, to retrieve the next three Wednesdays starting from 17th April, 2024:
iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]
However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what
you to achieve. Compare the results of both examples below:
# Adding one month after the other
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]
# Multiplying durations by an index
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]
The second example consistently points to the last day of the month,
as it performs operations on the duration, rather than shifting date
after date.
"""
@moduledoc since: "1.17.0"
@derive {Inspect, optional: [:year, :month, :week, :day, :hour, :minute, :second, :microsecond]}
defstruct year: 0,
month: 0,
week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0}
@typedoc """
The duration struct type.
"""
@type t :: %Duration{
year: integer,
month: integer,
week: integer,
day: integer,
hour: integer,
minute: integer,
second: integer,
microsecond: {integer, 0..6}
}
@typedoc """
The unit pair type specifies a pair of a valid duration unit key and value.
"""
@type unit_pair ::
{:year, integer}
| {:month, integer}
| {:week, integer}
| {:day, integer}
| {:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
@typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
"""
@type duration :: t | [unit_pair]
@microseconds_per_second 1_000_000
@doc """
Creates a new `Duration` struct from given `unit_pairs`.
Raises an `ArgumentError` when called with invalid unit pairs.
## Examples
iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1)
%Duration{year: 1, week: 3, hour: 4, second: 1}
iex> Duration.new!(second: 1, microsecond: {1000, 6})
%Duration{second: 1, microsecond: {1000, 6}}
iex> Duration.new!(month: 2)
%Duration{month: 2}
"""
@spec new!(duration()) :: t
def new!(%Duration{} = duration) do
duration
end
def new!(unit_pairs) do
Enum.each(unit_pairs, &validate_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_unit!({unit, _value})
when unit not in [:year, :month, :week, :day, :hour, :minute, :second] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond"
end
defp validate_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc """
Adds units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3}
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}}
"""
@spec add(t, t) :: t
def add(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year + d2.year,
month: d1.month + d2.month,
week: d1.week + d2.week,
day: d1.day + d2.day,
hour: d1.hour + d2.hour,
minute: d1.minute + d2.minute,
second: d1.second + d2.second,
microsecond: {m1 + m2, max(p1, p2)}
}
end
@doc """
Subtracts units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1}
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}}
"""
@spec subtract(t, t) :: t
def subtract(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year - d2.year,
month: d1.month - d2.month,
week: d1.week - d2.week,
day: d1.day - d2.day,
hour: d1.hour - d2.hour,
minute: d1.minute - d2.minute,
second: d1.second - d2.second,
microsecond: {m1 - m2, max(p1, p2)}
}
end
@doc """
Multiplies `duration` units by given `integer`.
## Examples
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}}
"""
@spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%Duration{
year: duration.year * integer,
month: duration.month * integer,
week: duration.week * integer,
day: duration.day * integer,
hour: duration.hour * integer,
minute: duration.minute * integer,
second: duration.second * integer,
microsecond: {ms * integer, p}
}
end
@doc """
Negates `duration` units.
## Examples
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}}
"""
@spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do
%Duration{
year: -duration.year,
month: -duration.month,
week: -duration.week,
day: -duration.day,
hour: -duration.hour,
minute: -duration.minute,
second: -duration.second,
microsecond: {-ms, p}
}
end
@doc """
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
## Examples
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
"""
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
def from_iso8601(string) when is_binary(string) do
case Calendar.ISO.parse_duration(string) do
{:ok, duration} ->
{:ok, new!(duration)}
error ->
error
end
end
@doc """
Same as `from_iso8601/1` but raises an `ArgumentError`.
## Examples
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}
"""
@spec from_iso8601!(String.t()) :: t
def from_iso8601!(string) when is_binary(string) do
case from_iso8601(string) do
{:ok, duration} ->
duration
{:error, reason} ->
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
end
end
@doc """
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
Note this function implements the *extension* of ISO 8601:2019. This extensions allows weeks to
appear between months and days: `P3M3W3D`, making it fully compatible with any `Duration` struct.
## Examples
iex> Duration.to_iso8601(Duration.new!(year: 3))
"P3Y"
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"P40DT12H42M12S"
iex> Duration.to_iso8601(Duration.new!(second: 30))
"PT30S"
iex> Duration.to_iso8601(Duration.new!([]))
"PT0S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
"PT1.002S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"PT-0.2000S"
"""
@spec to_iso8601(t) :: String.t()
def to_iso8601(%Duration{} = duration) do
case {to_iso8601_duration_date(duration), to_iso8601_duration_time(duration)} do
{[], []} -> "PT0S"
{date, time} -> IO.iodata_to_binary([?P, date, time])
end
end
defp to_iso8601_duration_date(%{year: 0, month: 0, week: 0, day: 0}) do
[]
end
defp to_iso8601_duration_date(%{year: year, month: month, week: week, day: day}) do
[pair(year, ?Y), pair(month, ?M), pair(week, ?W), pair(day, ?D)]
end
defp to_iso8601_duration_time(%{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
[]
end
defp to_iso8601_duration_time(%{hour: hour, minute: minute} = d) do
[?T, pair(hour, ?H), pair(minute, ?M), second_component(d)]
end
defp second_component(%{second: 0, microsecond: {0, _}}) do
[]
end
defp second_component(%{second: 0, microsecond: {_, 0}}) do
~c"0S"
end
defp second_component(%{second: second, microsecond: {_, 0}}) do
[Integer.to_string(second), ?S]
end
defp second_component(%{second: second, microsecond: {ms, p}}) do
total_ms = second * @microseconds_per_second + ms
second = total_ms |> div(@microseconds_per_second) |> abs()
ms = total_ms |> rem(@microseconds_per_second) |> abs()
sign = if total_ms < 0, do: ?-, else: []
[
sign,
Integer.to_string(second),
?.,
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p),
?S
]
end
@compile {:inline, pair: 2}
defp pair(0, _key), do: []
defp pair(num, key), do: [Integer.to_string(num), key]
end
+10 -327
View File
@@ -18,8 +18,7 @@ defmodule Calendar.ISO do
The standard library supports a minimal set of possible ISO 8601 features.
Specifically, the parser only supports calendar dates and does not support
ordinal and week formats. Additionally, it supports parsing ISO 8601
formatted durations, including negative time units and fractional seconds.
ordinal and week formats.
By default Elixir only parses extended-formatted date/times. You can opt-in
to parse basic-formatted date/times.
@@ -30,7 +29,7 @@ defmodule Calendar.ISO do
Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in the lowest component (such as
`10:01:25,5`).
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals.
#### Examples
@@ -231,9 +230,9 @@ defmodule Calendar.ISO do
]
end
defguardp is_year(year) when is_integer(year)
defguardp is_year_BCE(year) when year <= 0
defguardp is_year_CE(year) when year >= 1
defguardp is_year(year) when year in -9999..9999
defguardp is_year_BCE(year) when year in -9999..0
defguardp is_year_CE(year) when year in 1..9999
defguardp is_month(month) when month in 1..12
defguardp is_day(day) when day in 1..31
defguardp is_hour(hour) when hour in 0..23
@@ -664,87 +663,6 @@ defmodule Calendar.ISO do
end
end
@doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end
def parse_duration(_) do
{:error, :invalid_duration}
end
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
end
defp parse_duration_date(string, acc, allowed) do
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, [{key, integer} | acc], allowed)
else
_ -> {:error, :invalid_date_component}
end
end
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
case parse_microsecond(rest) do
{{ms, precision}, "S"} ->
ms =
case string do
"-" <> _ ->
-ms
_ ->
ms
end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
_ ->
{:error, :invalid_time_component}
end
{integer, <<next, rest::binary>>} ->
case find_unit(allowed, next) do
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
false -> {:error, :invalid_time_component}
end
_ ->
{:error, :invalid_time_component}
end
end
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_unit([], _unit), do: false
@doc """
Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -891,7 +809,7 @@ defmodule Calendar.ISO do
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
def date_from_iso_days(days) when days in -3_652_059..3_652_424 do
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
@@ -1446,7 +1364,7 @@ defmodule Calendar.ISO do
@spec valid_date?(year, month, day) :: boolean
def valid_date?(year, month, day)
when is_integer(year) and is_integer(month) and is_integer(day) do
is_month(month) and day in 1..days_in_month(year, month)
is_year(year) and is_month(month) and day in 1..days_in_month(year, month)
end
@doc """
@@ -1537,230 +1455,6 @@ defmodule Calendar.ISO do
{days, {@parts_per_day - 1, @parts_per_day}}
end
@doc """
Shifts Date by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2))
{2016, 3, 3}
iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1))
{2016, 3, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1))
{2016, 2, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1))
{2020, 2, 1}
"""
@impl true
@spec shift_date(year, month, day, Duration.t()) :: {year, month, day}
def shift_date(year, month, day, duration) do
shift_options = shift_date_options(duration)
Enum.reduce(shift_options, {year, month, day}, fn
{_, 0}, date ->
date
{:month, value}, date ->
shift_months(date, value)
{:day, value}, date ->
shift_days(date, value)
end)
end
@doc """
Shifts NaiveDateTime by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1))
{2016, 1, 3, 1, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30))
{2016, 1, 4, 6, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{2016, 1, 3, 0, 0, 0, {100, 6}}
"""
@impl true
@spec shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do
shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{_, 0}, naive_datetime ->
naive_datetime
{:month, value}, {year, month, day, hour, minute, second, microsecond} ->
{new_year, new_month, new_day} = shift_months({year, month, day}, value)
{new_year, new_month, new_day, hour, minute, second, microsecond}
{time_unit, value}, naive_datetime ->
shift_time_unit(naive_datetime, value, time_unit)
end)
end
@doc """
Shifts Time by Duration units according to its calendar.
## Examples
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2))
{15, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{13, 0, 0, {100, 6}}
"""
@impl true
@spec shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
def shift_time(hour, minute, second, microsecond, duration) do
shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{_, 0}, time ->
time
{time_unit, value}, time ->
shift_time_unit(time, value, time_unit)
end)
end
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
{year, month, day}
end
defp shift_months({year, month, day}, months) do
months_in_year = 12
total_months = year * months_in_year + month + months - 1
new_year = Integer.floor_div(total_months, months_in_year)
new_month =
case rem(total_months, months_in_year) + 1 do
new_month when new_month < 1 -> new_month + months_in_year
new_month -> new_month
end
new_day = min(day, days_in_month(new_year, new_month))
{new_year, new_month, new_day}
end
@doc false
def shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{year, month, day, hour, minute, second, {ms_value, _}} =
naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> shift_time_unit(value, unit)
|> naive_datetime_from_iso_days()
{year, month, day, hour, minute, second, {ms_value, precision}}
end
def shift_time_unit({hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{_days, day_fraction} =
shift_time_unit({0, time_to_day_fraction(hour, minute, second, microsecond)}, value, unit)
{hour, minute, second, {microsecond, _}} = time_from_day_fraction(day_fraction)
{hour, minute, second, {microsecond, precision}}
end
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
defp shift_time_unit_values({0, _}, {_, original_precision}) do
{0, original_precision}
end
defp shift_time_unit_values({ms_value, ms_precision}, {_, _}) do
{ms_value, ms_precision}
end
defp shift_time_unit_values(value, {_, original_precision}) do
{value, original_precision}
end
defp shift_date_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0}
}) do
[
month: year * 12 + month,
day: week * 7 + day
]
end
defp shift_date_options(_duration) do
raise ArgumentError,
"cannot shift date by time scale unit. Expected :year, :month, :week, :day"
end
defp shift_datetime_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
month: year * 12 + month,
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(%Duration{
year: 0,
month: 0,
week: 0,
day: 0,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
second: hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(_duration) do
raise ArgumentError,
"cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond"
end
## Helpers
@doc false
@@ -1948,22 +1642,11 @@ defmodule Calendar.ISO do
defp days_in_previous_years(0), do: 0
# A concise version of the algorithm would use floor_div instead of div.
# However, floor_div would check the operands on every operation.
# We optimize this by providing a positive and negative version of each algorithm.
defp days_in_previous_years(year) when year > 0 do
defp days_in_previous_years(year) do
previous_year = year - 1
div(previous_year, 4) - div(previous_year, 100) +
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
defp days_in_previous_years(year) when year < 0 do
previous_year = year - 1
div(year, 4) - div(year, 100) +
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
Integer.floor_div(previous_year, 4) - Integer.floor_div(previous_year, 100) +
Integer.floor_div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
+10 -115
View File
@@ -448,8 +448,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
"""
@doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -468,21 +466,23 @@ defmodule NaiveDateTime do
end
def add(
%{calendar: calendar, microsecond: {_, precision}} = naive_datetime,
%{microsecond: {_, precision}, calendar: calendar} = naive_datetime,
amount_to_add,
unit
)
when is_integer(amount_to_add) do
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
naive_datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add, ppd)
|> from_iso_days(calendar, precision)
end
@@ -571,83 +571,6 @@ defmodule NaiveDateTime do
units1 - units2
end
@doc """
Shifts given `naive_datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1)
~N[2016-02-29 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1)
~N[2020-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: -2, day: 1)
~N[2014-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45)
~N[2016-01-31 00:00:45]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6})
~N[2016-01-31 00:00:00.000100]
# leap years
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 1)
~N[2025-02-28 00:00:00]
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4)
~N[2028-02-29 00:00:00]
# rounding down
iex> NaiveDateTime.shift(~N[2015-01-31 00:00:00], month: 1)
~N[2015-02-28 00:00:00]
"""
@doc since: "1.17.0"
@spec shift(Calendar.naive_datetime(), Duration.duration()) :: t
def shift(%{calendar: calendar} = naive_datetime, duration) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = naive_datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%NaiveDateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given naive datetime with the microsecond field truncated to the
given precision (`:microsecond`, `:millisecond` or `:second`).
@@ -1053,28 +976,11 @@ defmodule NaiveDateTime do
"""
@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)
def from_gregorian_seconds(seconds, {microsecond, precision}, Calendar.ISO)
when is_integer(seconds) do
{days, seconds} = div_rem(seconds, 24 * 60 * 60)
{hours, seconds} = div_rem(seconds, 60 * 60)
{minutes, seconds} = div_rem(seconds, 60)
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
%NaiveDateTime{
calendar: Calendar.ISO,
year: year,
month: month,
day: day,
hour: hours,
minute: minutes,
second: seconds,
microsecond: {microsecond, precision}
}
end
def from_gregorian_seconds(seconds, {microsecond, precision}, calendar)
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)
@@ -1093,17 +999,6 @@ defmodule NaiveDateTime do
}
end
defp div_rem(int1, int2) do
div = div(int1, int2)
rem = int1 - div * int2
if rem >= 0 do
{div, rem}
else
{div - 1, rem + int2}
end
end
@doc """
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
+17 -103
View File
@@ -50,6 +50,7 @@ defmodule Time do
calendar: Calendar.calendar()
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
@@ -499,8 +500,6 @@ defmodule Time do
iex> result.microsecond
{21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -516,26 +515,13 @@ defmodule Time do
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
when is_integer(amount_to_add) do
valid? =
if is_integer(unit),
do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a
unless valid? do
raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
total = time_to_microseconds(time) + amount_to_add
parts = Integer.mod(total, @parts_per_day)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
{hour, minute, second, {microsecond, _precision}} =
Calendar.ISO.shift_time_unit(
{hour, minute, second, microsecond},
amount_to_add,
unit
)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
%Time{
hour: hour,
@@ -546,91 +532,19 @@ defmodule Time do
}
end
@doc """
Shifts given `time` by `duration` according to its calendar.
Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed to seconds and
microseconds before they are applied.
Raises an `ArgumentError` when called with date scale units.
## Examples
iex> Time.shift(~T[01:00:15], hour: 12)
~T[13:00:15]
iex> Time.shift(~T[01:35:00], hour: 6, minute: -15)
~T[07:20:00]
iex> Time.shift(~T[01:15:00], second: 125)
~T[01:17:05]
iex> Time.shift(~T[01:00:15], microsecond: {100, 6})
~T[01:00:15.000100]
iex> Time.shift(~T[01:15:00], Duration.new!(second: 65))
~T[01:16:05]
"""
@doc since: "1.17.0"
@spec shift(Calendar.time(), Duration.t() | [unit_pair]) :: t
when unit_pair:
{:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
def shift(%{calendar: calendar} = time, duration) do
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
{hour, minute, second, microsecond} =
calendar.shift_time(hour, minute, second, microsecond, __duration__!(duration))
%Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
defp time_to_microseconds(%{
calendar: Calendar.ISO,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _}
}) do
0
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_duration_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_duration_unit!({unit, _value}) when unit in [:year, :month, :week, :day] do
raise ArgumentError,
"unsupported unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({unit, _value})
when unit not in [:hour, :minute, :second, :microsecond] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
defp time_to_microseconds(time) do
iso_days = {0, to_day_fraction(time)}
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
end
@doc """
+25 -36
View File
@@ -3,14 +3,14 @@ defmodule Code do
Utilities for managing code compilation, code evaluation, and code loading.
This module complements Erlang's [`:code` module](`:code`)
to add behavior which is specific to Elixir. For functions to
to add behaviour which is specific to Elixir. For functions to
manipulate Elixir's AST (rather than evaluating it), see the
`Macro` module.
## Working with files
This module contains three functions for compiling and evaluating files.
Here is a summary of them and their behavior:
Here is a summary of them and their behaviour:
* `require_file/2` - compiles a file and tracks its name. It does not
compile the file again if it has been previously required.
@@ -159,7 +159,7 @@ defmodule Code do
`{:ok, value}` or `:error`.
* `:defmodule` - (since v1.16.2) traced as soon as the definition of a module
starts. This is invoked early on in the module life cycle, `Module.open?/1`
starts. This is invoked early on in the module life-cycle, `Module.open?/1`
still returns `false` for such traces
* `{:on_module, bytecode, _ignore}` - (since v1.13.0) traced whenever a module
@@ -665,9 +665,9 @@ defmodule Code do
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to 98. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
the document. Defaults to 98. Note this value is used as
guideline but there are situations where it is not enforced.
See the "Line length" section below for more information
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
@@ -714,8 +714,8 @@ defmodule Code do
specially because a function is named `defmodule`, `def`, or the like. This
principle mirrors Elixir's goal of being an extensible language where
developers can extend the language with new constructs as if they were
part of the language. When it is absolutely necessary to change behavior
based on the name, this behavior should be configurable, such as the
part of the language. When it is absolutely necessary to change behaviour
based on the name, this behaviour should be configurable, such as the
`:locals_without_parens` option.
## Running the formatter
@@ -798,10 +798,9 @@ defmodule Code do
## Line length
Another point about the formatter is that the `:line_length` configuration
indicates when an expression should be broken over multiple lines but it is
not guaranteed to do so. In many cases, it is not possible for the formatter
to break your code apart, which means it will go over the line length.
For example, if you have a long string:
is a guideline. In many cases, it is not possible for the formatter to break
your code apart, which means it will go over the line length. For example,
if you have a long string:
"this is a very long string that will go over the line length"
@@ -814,15 +813,15 @@ defmodule Code do
The string concatenation makes the code fit on a single line and also
gives more options to the formatter.
This may also appear in keywords such as do/end blocks and operators,
where the `do` keyword may go over the line length because there is no
opportunity for the formatter to introduce a line break in a readable way.
For example, if you do:
This may also appear in do/end blocks, where the `do` keyword (or `->`)
may go over the line length because there is no opportunity for the
formatter to introduce a line break in a readable way. For example,
if you do:
case very_long_expression() do
end
And only the `do` keyword is beyond the line length, Elixir **will not**
And only the `do` keyword is above the line length, Elixir **will not**
emit this:
case very_long_expression()
@@ -860,7 +859,7 @@ defmodule Code do
* Newlines before certain operators (such as the pipeline operators)
and before other operators (such as comparison operators)
The behaviors above are not guaranteed. We may remove or add new
The behaviours above are not guaranteed. We may remove or add new
rules in the future. The goal of documenting them is to provide better
understanding on what to expect from the formatter.
@@ -1129,7 +1128,7 @@ defmodule Code do
For example, `"null byte\\t\\x00"` will be kept as is instead of being
converted to a bitstring literal. Note if you set this option to false, the
resulting AST is no longer valid, but it can be useful to analyze/transform
source code, typically in combination with `quoted_to_algebra/2`.
source code, typically in in combination with `quoted_to_algebra/2`.
Defaults to `true`.
* `:existing_atoms_only` - when `true`, raises an error
@@ -1150,7 +1149,7 @@ defmodule Code do
* `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this
option overrides the `:existing_atoms_only` behavior for static
option overrides the `:existing_atoms_only` behaviour for static
atoms but `:existing_atoms_only` is still used for dynamic atoms,
such as atoms with interpolations.
@@ -1219,7 +1218,6 @@ defmodule Code do
It returns the AST if it succeeds,
raises an exception otherwise. The exception is a `TokenMissingError`
in case a token is missing (usually because the expression is incomplete),
`MismatchedDelimiterError` (in case of mismatched opening and closing delimiters) and
`SyntaxError` otherwise.
Check `string_to_quoted/2` for options information.
@@ -1243,7 +1241,7 @@ defmodule Code do
Comments are maps with the following fields:
* `:line` - The line number of the source code
* `:line` - The line number the source code
* `:text` - The full text of the comment, including the leading `#`
@@ -1633,9 +1631,8 @@ defmodule Code do
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
This `:warn` behaviour only exists for compatibility reasons when working
with old dependencies.
It always returns `:ok`. Raises an error for invalid options.
@@ -1686,7 +1683,6 @@ defmodule Code do
end
# TODO: Make this option have no effect on Elixir v2.0
# TODO: Warn if mode is :warn on Elixir v1.19
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
:elixir_config.put(:on_undefined_variable, value)
:ok
@@ -1725,7 +1721,7 @@ defmodule Code do
Returns a list of tuples where the first element is the module name
and the second one is its bytecode (as a binary). A `file` can be
given as a second argument which will be used for reporting warnings
given as second argument which will be used for reporting warnings
and errors.
**Warning**: `string` can be any Elixir code and code can be executed with
@@ -1969,7 +1965,7 @@ defmodule Code do
@doc """
Returns `true` if the module is loaded.
This function doesn't attempt to load the module. For such behavior,
This function doesn't attempt to load the module. For such behaviour,
`ensure_loaded?/1` can be used.
## Examples
@@ -2036,11 +2032,7 @@ defmodule Code do
@spec fetch_docs(module | String.t()) ::
{:docs_v1, annotation, beam_language, format, module_doc :: doc_content, metadata,
docs :: [doc_element]}
| {:error,
:module_not_found
| :chunk_not_found
| {:invalid_chunk, binary}
| :invalid_beam}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | atom(),
doc_content: %{optional(binary) => binary} | :none | :hidden,
@@ -2111,9 +2103,6 @@ defmodule Code do
{:error, :beam_lib, {:file_error, _, :enoent}} ->
{:error, :module_not_found}
{:error, :beam_lib, _} ->
{:error, :invalid_beam}
end
end
+12 -25
View File
@@ -290,7 +290,6 @@ defmodule Code.Formatter do
end
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp quoted_to_algebra(
{{:., _, [List, :to_charlist]}, meta, [entries]} = quoted,
context,
@@ -367,11 +366,6 @@ defmodule Code.Formatter do
tuple_to_algebra(meta, [left, right], :flex_break, state)
end
# (left -> right)
defp quoted_to_algebra({:__block__, _, [[{:->, _, _} | _] = clauses]}, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
case meta[:delimiter] do
~s['''] ->
@@ -422,7 +416,6 @@ defmodule Code.Formatter do
{Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state}
end
# (unquote_splicing(...))
defp quoted_to_algebra(
{:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]},
context,
@@ -486,11 +479,6 @@ defmodule Code.Formatter do
end
end
# ...
defp quoted_to_algebra({:..., _meta, []}, _context, state) do
{"...", state}
end
# 1..2//3
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
@@ -511,6 +499,11 @@ defmodule Code.Formatter do
remote_to_algebra(quoted, context, state)
end
# (left -> right)
defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
# [keyword: :list] (inner part)
# %{:foo => :bar} (inner part)
defp quoted_to_algebra(list, context, state) when is_list(list) do
@@ -637,7 +630,7 @@ defmodule Code.Formatter do
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
with [left, right] <- args,
{_, _} <- augmented_binary_op(fun) do
{_, _} <- Code.Identifier.binary_op(fun) do
binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state)
else
_ -> :error
@@ -664,7 +657,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting)
when op in @right_new_line_before_binary_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = op_string <> " "
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -701,7 +694,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting)
when op in @pipeline_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
max_line = line(meta)
@@ -715,7 +708,7 @@ defmodule Code.Formatter do
{{doc, @empty, 1}, state}
{{op, context}, arg}, _args, state ->
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = Atom.to_string(op) <> " "
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
{{concat(op_string, doc), @empty, 1}, state}
@@ -725,7 +718,7 @@ defmodule Code.Formatter do
end
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -784,7 +777,7 @@ defmodule Code.Formatter do
{parent_assoc, parent_prec} = parent_info
with {op, meta, [left, right]} <- operand,
op_info = augmented_binary_op(op),
op_info = Code.Identifier.binary_op(op),
{_assoc, prec} <- op_info do
op_string = Atom.to_string(op)
@@ -2203,15 +2196,10 @@ defmodule Code.Formatter do
unary_operator?(quoted) or binary_operator?(quoted)
end
# We convert ..// into two operators for simplicity,
# so we need to augment the binary table.
defp augmented_binary_op(:"//"), do: {:right, 190}
defp augmented_binary_op(op), do: Code.Identifier.binary_op(op)
defp binary_operator?(quoted) do
case quoted do
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
{op, _, [_, _]} when is_atom(op) -> augmented_binary_op(op) != :error
{op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error
_ -> false
end
end
@@ -2247,7 +2235,6 @@ defmodule Code.Formatter do
(not interpolated?(entries) and eol_or_comments?(meta, state))
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
meta[:delimiter] == ~s[''']
end
+7 -39
View File
@@ -31,7 +31,7 @@ defmodule Code.Fragment do
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, ~c"hello_wor"}
{:local_or_var, 'hello_wor'}
## Return values
@@ -481,10 +481,10 @@ defmodule Code.Fragment do
defp operator(rest, count, acc, _call_op?) do
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
{:ok, _, _, _, [{:atom, _, _}], []} ->
{:ok, _, _, _, [{:atom, _, _}]} ->
{{:unquoted_atom, tl(acc)}, count}
{:ok, _, _, _, [{_, _, op}], []} ->
{:ok, _, _, _, [{_, _, op}]} ->
{rest, dot_count} = strip_spaces(rest, count)
cond do
@@ -547,7 +547,7 @@ defmodule Code.Fragment do
## Examples
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
%{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}}
## Differences to `cursor_context/2`
@@ -1088,41 +1088,9 @@ defmodule Code.Fragment do
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [cursor_completion: true, emit_warnings: false] ++ opts
opts =
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens = :lists.reverse(rev_tokens, rev_terminators)
case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, ast} ->
{:ok, ast}
{:error, error} ->
# In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
tokens =
:lists.reverse(
rev_tokens,
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
Enum.map(rev_terminators, fn tuple ->
{line, column, info} = elem(tuple, 1)
put_elem(tuple, 1, {line, column + 5, info})
end)
)
case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, ast} -> {:ok, ast}
{:error, _} -> {:error, error}
end
end
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts)
end
end
+2 -1
View File
@@ -13,7 +13,7 @@ defmodule Code.Identifier do
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
def unary_op(op) do
cond do
op in [:&, :...] -> {:non_associative, 90}
op in [:&] -> {:non_associative, 90}
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
op in [:@] -> {:non_associative, 320}
true -> :error
@@ -44,6 +44,7 @@ defmodule Code.Identifier do
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
op in [:in] -> {:left, 170}
op in [:"^^^"] -> {:left, 180}
op in [:"//"] -> {:right, 190}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
+17 -11
View File
@@ -96,7 +96,6 @@ defmodule Code.Normalizer do
end
# Charlists with interpolations
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
@@ -137,6 +136,11 @@ defmodule Code.Normalizer do
{:., meta, [left, right]}
end
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
normalize_args(quoted, state)
end
# left -> right
defp do_normalize({:->, meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
@@ -165,6 +169,9 @@ defmodule Code.Normalizer do
right = normalize_map_args(right, state)
[{:|, pipe_meta, [left, right]}]
[{_, _, _} = call] ->
[do_normalize(call, state)]
args ->
normalize_map_args(args, state)
end
@@ -174,17 +181,16 @@ defmodule Code.Normalizer do
# Sigils
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
with "sigil_" <> _ <- Atom.to_string(sigil),
true <- binary_interpolated?(args),
true <- List.ascii_printable?(modifiers) do
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
when is_list(args) and is_atom(sigil) do
case Atom.to_string(sigil) do
"sigil_" <> _ ->
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
else
_ ->
normalize_call(quoted, state)
end
+2 -14
View File
@@ -94,10 +94,10 @@ end
defimpl Collectable, for: List do
def into(list) do
# TODO: Change the behavior so the into always comes last on Elixir v2.0
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
if list != [] do
IO.warn(
"the Collectable protocol is deprecated for non-empty lists. The behavior of " <>
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
@@ -140,10 +140,6 @@ defimpl Collectable, for: BitString do
__acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a binary requires a bitstring, got: #{inspect(other)}"
end
{[binary], fun}
@@ -159,10 +155,6 @@ defimpl Collectable, for: BitString do
_acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a bitstring requires a bitstring, got: #{inspect(other)}"
end
{bitstring, fun}
@@ -180,10 +172,6 @@ defimpl Collectable, for: Map do
_map_acc, :halt ->
:ok
_map_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a map requires {key, value} tuples, got: #{inspect(other)}"
end
{map, fun}
+3 -3
View File
@@ -82,9 +82,9 @@ defmodule Config do
...
end
The only places where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which are always within
the `Mix.Tasks` namespace.
The only files where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which always within the
`Mix.Tasks` namespace.
## `config/runtime.exs`
+1 -1
View File
@@ -312,7 +312,7 @@ defmodule Config.Provider do
"""
the application #{inspect(app)} has a different value set #{path(key, path)} \
during runtime compared to compile time. Since this application environment entry was \
marked as compile time, this difference can lead to different behavior than expected:
marked as compile time, this difference can lead to different behaviour than expected:
* Compile time value #{return_to_text(compile_return)}
* Runtime value #{return_to_text(runtime_return)}
+9
View File
@@ -1136,6 +1136,15 @@ defmodule DynamicSupervisor do
]
end
@impl true
def format_status(:terminate, [_pdict, state]) do
state
end
def format_status(_, [_pdict, %{mod: mod} = state]) do
[data: [{~c"State", state}], supervisor: [{~c"Callback", mod}]]
end
## Helpers
@compile {:inline, call: 2}
+91 -110
View File
@@ -80,7 +80,7 @@ defprotocol Enumerable do
Returns the accumulator for the next enumeration step.
"""
@type reducer :: (element :: term, element_acc :: term -> acc)
@type reducer :: (element :: term, current_acc :: acc -> updated_acc :: acc)
@typedoc """
The result of the reduce operation.
@@ -269,12 +269,12 @@ defmodule Enum do
After all, if we want to traverse every element on a list, the longer the
list, the more elements we need to traverse, and the longer it will take.
This linear behavior should also be expected on operations like `count/1`,
This linear behaviour should also be expected on operations like `count/1`,
`member?/2`, `at/2` and similar. While Elixir does allow data types to
provide performant variants for such operations, you should not expect it
to always be available, since the `Enum` module is meant to work with a
large variety of data types and not all data types can provide optimized
behavior.
behaviour.
Finally, note the functions in the `Enum` module are eager: they will
traverse the enumerable as soon as they are invoked. This is particularly
@@ -1096,10 +1096,6 @@ defmodule Enum do
iex> Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
[2]
iex> Enum.filter(["apple", "pear", "banana"], fn fruit -> String.contains?(fruit, "a") end)
["apple", "pear", "banana"]
iex> Enum.filter([4, 21, 24, 904], fn seconds -> seconds > 1000 end)
[]
Keep in mind that `filter` is not capable of filtering and
transforming an element at the same time. If you would like
@@ -1432,7 +1428,7 @@ defmodule Enum do
)
# Avoid warnings about Dict
dict_module = String.to_atom("Dict")
dict_module = Dict
reduce(reverse(enumerable), dict, fn entry, categories ->
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
@@ -1532,7 +1528,8 @@ defmodule Enum do
defp into_map(enumerable) when is_list(enumerable), do: :maps.from_list(enumerable)
defp into_map(enumerable), do: enumerable |> Enum.to_list() |> :maps.from_list()
defp into_map(%{} = enumerable, collectable), do: Map.merge(collectable, enumerable)
defp into_map(%{} = enumerable, collectable),
do: Map.merge(collectable, enumerable)
defp into_map(enumerable, collectable) when is_list(enumerable),
do: Map.merge(collectable, :maps.from_list(enumerable))
@@ -1639,8 +1636,8 @@ defmodule Enum do
If `joiner` is not passed at all, it defaults to an empty string.
All elements in the `enumerable` must be convertible to a string
or be a binary, otherwise an error is raised.
All elements in the `enumerable` must be convertible to a string,
otherwise an error is raised.
## Examples
@@ -1654,7 +1651,7 @@ defmodule Enum do
"ab cdefg h i"
"""
@spec join(t, binary()) :: binary()
@spec join(t, String.t()) :: String.t()
def join(enumerable, joiner \\ "")
def join(enumerable, "") do
@@ -2365,6 +2362,12 @@ defmodule Enum do
the random value. Check its documentation for setting a
different random algorithm or a different seed.
The implementation is based on the
[reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling#Relation_to_Fisher-Yates_shuffle)
algorithm.
It assumes that the sample being returned can fit into memory;
the input `enumerable` doesn't have to, as it is traversed just once.
If a range is passed into the function, this function will pick a
random value between the range limits, without traversing the whole
range (thus executing in constant time and constant memory).
@@ -2383,12 +2386,6 @@ defmodule Enum do
iex> Enum.random(1..1_000)
309
## Implementation
The random functions in this module implement reservoir sampling,
which allows them to sample infinite collections. In particular,
we implement Algorithm L, as described in by Kim-Hung Li in
"Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))".
"""
@spec random(t) :: element
def random(enumerable)
@@ -2736,7 +2733,7 @@ defmodule Enum do
end
# Normalize negative input ranges like Enum.slice/2
def slide(enumerable, first..last//_, insertion_index)
def slide(enumerable, first..last, insertion_index)
when first < 0 or last < 0 or insertion_index < 0 do
count = Enum.count(enumerable)
normalized_first = if first >= 0, do: first, else: Kernel.max(first + count, 0)
@@ -2753,23 +2750,23 @@ defmodule Enum do
end
end
def slide(enumerable, insertion_index.._//_, insertion_index) do
def slide(enumerable, insertion_index.._, insertion_index) do
Enum.to_list(enumerable)
end
def slide(_, first..last//_, insertion_index)
def slide(_, first..last, insertion_index)
when insertion_index > first and insertion_index <= last do
raise ArgumentError,
"insertion index for slide must be outside the range being moved " <>
"(tried to insert #{first}..#{last} at #{insertion_index})"
end
def slide(enumerable, first..last//_, _insertion_index) when first > last do
def slide(enumerable, first..last, _insertion_index) when first > last do
Enum.to_list(enumerable)
end
# Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last)
def slide(enumerable, first..last//_, insertion_index) do
def slide(enumerable, first..last, insertion_index) do
impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4
cond do
@@ -2905,11 +2902,11 @@ defmodule Enum do
the default from Erlang/OTP 22:
# Although not necessary, let's seed the random algorithm
iex> :rand.seed(:exsss, {11, 22, 33})
iex> :rand.seed(:exsss, {1, 2, 3})
iex> Enum.shuffle([1, 2, 3])
[3, 2, 1]
iex> Enum.shuffle([1, 2, 3])
[2, 1, 3]
iex> Enum.shuffle([1, 2, 3])
[2, 3, 1]
"""
@spec shuffle(t) :: list
@@ -2919,12 +2916,9 @@ defmodule Enum do
[{:rand.uniform(), x} | acc]
end)
shuffle_unwrap(:lists.keysort(1, randomized))
shuffle_unwrap(:lists.keysort(1, randomized), [])
end
defp shuffle_unwrap([{_, h} | rest]), do: [h | shuffle_unwrap(rest)]
defp shuffle_unwrap([]), do: []
@doc """
Returns a subset list of the given `enumerable` by `index_range`.
@@ -3594,115 +3588,101 @@ defmodule Enum do
# Although not necessary, let's seed the random algorithm
iex> :rand.seed(:exsss, {1, 2, 3})
iex> Enum.take_random(1..10, 2)
[6, 1]
[3, 1]
iex> Enum.take_random(?a..?z, 5)
~c"bkzmt"
'mikel'
"""
@spec take_random(t, non_neg_integer) :: list
def take_random(enumerable, count)
def take_random(_enumerable, 0), do: []
def take_random([], _), do: []
def take_random([h | t], 1), do: take_random_list_one(t, h, 1)
def take_random(enumerable, 1) do
enumerable
|> reduce({0, 0, 1.0, nil}, fn
elem, {idx, idx, w, _current} ->
{jdx, w} = take_jdx_w(idx, w, 1)
{idx + 1, jdx, w, elem}
|> reduce([], fn
x, [current | index] ->
if :rand.uniform(index + 1) == 1 do
[x | index + 1]
else
[current | index + 1]
end
_elem, {idx, jdx, w, current} ->
{idx + 1, jdx, w, current}
x, [] ->
[x | 1]
end)
|> case do
{0, 0, 1.0, nil} -> []
{_idx, _jdx, _w, current} -> [current]
[] -> []
[current | _index] -> [current]
end
end
def take_random(enumerable, count) when count in 0..128 do
def take_random(enumerable, count) when is_integer(count) and count in 0..128 do
sample = Tuple.duplicate(nil, count)
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> put_elem(idx, elem(sample, rand)) |> put_elem(rand, elem)
reducer = fn elem, {idx, sample} ->
jdx = random_index(idx)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
cond do
idx < count ->
value = elem(sample, jdx)
{idx + 1, put_elem(sample, idx, value) |> put_elem(jdx, elem)}
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, put_elem(sample, pos, elem)}
jdx < count ->
{idx + 1, put_elem(sample, jdx, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
true ->
{idx + 1, sample}
end
end
{size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, sample}, reducer)
if count < size do
Tuple.to_list(sample)
else
take_tupled(sample, size, [])
end
{size, sample} = reduce(enumerable, {0, sample}, reducer)
sample |> Tuple.to_list() |> take(Kernel.min(count, size))
end
def take_random(enumerable, count) when is_integer(count) and count >= 0 do
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> Map.put(idx, Map.get(sample, rand)) |> Map.put(rand, elem)
reducer = fn elem, {idx, sample} ->
jdx = random_index(idx)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
cond do
idx < count ->
value = Map.get(sample, jdx)
{idx + 1, Map.put(sample, idx, value) |> Map.put(jdx, elem)}
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, %{sample | pos => elem}}
jdx < count ->
{idx + 1, Map.put(sample, jdx, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
true ->
{idx + 1, sample}
end
end
{size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, %{}}, reducer)
take_mapped(sample, Kernel.min(count, size), [])
{size, sample} = reduce(enumerable, {0, %{}}, reducer)
take_random(sample, Kernel.min(count, size), [])
end
@compile {:inline, take_jdx_w: 3, take_index: 1}
defp take_jdx_w(idx, w, count) do
w = w * :math.exp(:math.log(:rand.uniform()) / count)
jdx = idx + floor(:math.log(:rand.uniform()) / :math.log(1 - w)) + 1
{jdx, w}
end
defp take_random(_sample, 0, acc), do: acc
defp take_index(0), do: 0
defp take_index(idx), do: :rand.uniform(idx + 1) - 1
defp take_tupled(_sample, 0, acc), do: acc
defp take_tupled(sample, position, acc) do
defp take_random(sample, position, acc) do
position = position - 1
take_tupled(sample, position, [elem(sample, position) | acc])
take_random(sample, position, [Map.get(sample, position) | acc])
end
defp take_mapped(_sample, 0, acc), do: acc
defp take_mapped(sample, position, acc) do
position = position - 1
take_mapped(sample, position, [Map.fetch!(sample, position) | acc])
defp take_random_list_one([h | t], current, index) do
if :rand.uniform(index + 1) == 1 do
take_random_list_one(t, h, index + 1)
else
take_random_list_one(t, current, index + 1)
end
end
defp take_random_list_one([], current, _), do: [current]
defp random_index(0), do: 0
defp random_index(idx), do: :rand.uniform(idx + 1) - 1
@doc """
Takes the elements from the beginning of the `enumerable` while `fun` returns
a truthy value.
@@ -3843,7 +3823,7 @@ defmodule Enum do
@doc """
Returns the `enumerable` with each element wrapped in a tuple
alongside its index or according to a given function.
alongside its index.
May receive a function or an integer offset.
@@ -3893,10 +3873,6 @@ defmodule Enum do
Zips corresponding elements from two enumerables into a list
of tuples.
Because a list of two-element tuples with atoms as the first
tuple element is a keyword list (`Keyword`), zipping a first list
of atoms with a second list of any kind creates a keyword list.
The zipping finishes as soon as either enumerable completes.
## Examples
@@ -3904,9 +3880,6 @@ defmodule Enum do
iex> Enum.zip([1, 2, 3], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
iex> Enum.zip([:a, :b, :c], [1, 2, 3])
[a: 1, b: 2, c: 3]
iex> Enum.zip([1, 2, 3, 4, 5], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
@@ -4027,7 +4000,7 @@ defmodule Enum do
@doc """
Reduces over two enumerables halting as soon as either enumerable is empty.
In practice, the behavior provided by this function can be achieved with:
In practice, the behaviour provided by this function can be achieved with:
Enum.reduce(Stream.zip(left, right), acc, reducer)
@@ -4061,7 +4034,7 @@ defmodule Enum do
The reducer will receive 2 args: a list of elements (one from each enum) and the
accumulator.
In practice, the behavior provided by this function can be achieved with:
In practice, the behaviour provided by this function can be achieved with:
Enum.reduce(Stream.zip(enums), acc, reducer)
@@ -4463,6 +4436,14 @@ defmodule Enum do
[acc | scan_list(rest, acc, fun)]
end
## shuffle
defp shuffle_unwrap([{_, h} | enumerable], t) do
shuffle_unwrap(enumerable, [h | t])
end
defp shuffle_unwrap([], t), do: t
## slice
defp slice_forward(enumerable, start, amount, step) when start < 0 do
+57 -58
View File
@@ -1,18 +1,11 @@
defmodule Exception do
@moduledoc """
Functions for dealing with throw/catch/exit and exceptions.
Functions to format throw/catch/exit and exceptions.
This module also defines the behaviour required by custom
exceptions. To define your own, see `defexception/1`.
Note that stacktraces in Elixir are only available inside
catch and rescue by using the `__STACKTRACE__/0` variable.
## Formatting functions
Several functions in this module help format exceptions.
Some of these functions expect the stacktrace as argument.
The stacktrace is typically available inside catch and
rescue by using the `__STACKTRACE__/0` variable.
Do not rely on the particular format returned by the
Do not rely on the particular format returned by the `format*`
functions in this module. They may be changed in future releases
in order to better suit Elixir's tool chain. In other words,
by using the functions in this module it is guaranteed you will
@@ -1048,6 +1041,44 @@ defmodule ArgumentError do
"""
defexception message: "argument error"
@impl true
def blame(
exception,
[{:erlang, :apply, [module, function, args], _} | _] = stacktrace
) do
message =
cond do
not proper_list?(args) ->
"you attempted to apply a function named #{inspect(function)} on module #{inspect(module)} " <>
"with arguments #{inspect(args)}. Arguments (the third argument of apply) must always be a proper list"
# Note that args may be an empty list even if they were supplied
not is_atom(module) and is_atom(function) and args == [] ->
"you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <>
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
"If you are using the dot syntax, such as module.function(), " <>
"make sure the left-hand side of the dot is a module atom"
not is_atom(module) ->
"you attempted to apply a function on #{inspect(module)}. " <>
"Modules (the first argument of apply) must always be an atom"
not is_atom(function) ->
"you attempted to apply a function named #{inspect(function)} on module #{inspect(module)}. " <>
"However, #{inspect(function)} is not a valid function name. Function names (the second argument " <>
"of apply) must always be an atom"
end
{%{exception | message: message}, stacktrace}
end
def blame(exception, stacktrace) do
{exception, stacktrace}
end
defp proper_list?(list) when length(list) >= 0, do: true
defp proper_list?(_), do: false
end
defmodule ArithmeticError do
@@ -1212,7 +1243,7 @@ defmodule SyntaxError do
})
when not is_nil(snippet) and not is_nil(column) do
snippet =
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
format_message(file, line, column, snippet)
end
@@ -1225,7 +1256,7 @@ defmodule SyntaxError do
description: description
}) do
snippet =
:elixir_errors.format_snippet(:error, {line, column}, file, description, nil, %{})
:elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil)
padded = " " <> String.replace(snippet, "\n", "\n ")
format_message(file, line, column, padded)
@@ -1316,7 +1347,7 @@ defmodule TokenMissingError do
description: description
}) do
snippet =
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
format_message(file, line, column, snippet)
end
@@ -2043,17 +2074,10 @@ defmodule KeyError do
defp message(key, term) do
message = "key #{inspect(key)} not found"
cond do
term == nil ->
message
is_atom(term) and is_atom(key) ->
message <>
" in: #{inspect(term)} (if instead you want to invoke #{inspect(term)}.#{key}(), " <>
"make sure to add parentheses after the function name)"
true ->
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
if term != nil do
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
else
message
end
end
@@ -2062,6 +2086,11 @@ defmodule KeyError do
{exception, stacktrace}
end
def blame(exception = %{term: nil}, stacktrace) do
message = message(exception.key, exception.term)
{%{exception | message: message}, stacktrace}
end
def blame(exception, stacktrace) do
%{term: term, key: key} = exception
message = message(key, term)
@@ -2278,39 +2307,9 @@ defmodule ErlangError do
@doc false
def normalize(:badarg, stacktrace) do
case stacktrace do
[{:erlang, :apply, [module, function, args], _} | _] when not is_atom(module) ->
message =
cond do
is_map(module) and is_atom(function) and is_map_key(module, function) ->
"you attempted to apply a function named #{inspect(function)} on a map/struct. " <>
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
if is_function(module[function]) do
"If you are trying to invoke an anonymous function in a map/struct, " <>
"add a dot between the function name and the parenthesis: map.#{function}.()"
else
"If you are using the dot syntax, ensure there are no parentheses " <>
"after the field name, such as map.#{function}"
end
is_atom(function) and args == [] ->
"you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <>
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
"If you are using the dot syntax, such as module.function(), " <>
"make sure the left-hand side of the dot is an atom representing a module"
true ->
"you attempted to apply a function on #{inspect(module)}. " <>
"Modules (the first argument of apply) must always be an atom"
end
%ArgumentError{message: message}
_ ->
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
:error -> %ArgumentError{}
end
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
:error -> %ArgumentError{}
end
end
+29 -44
View File
@@ -101,7 +101,6 @@ defmodule File do
@type posix :: :file.posix()
@type io_device :: :file.io_device()
@type file_descriptor :: :file.fd()
@type stat_options :: [time: :local | :universal | :posix]
@type mode ::
:append
@@ -759,7 +758,7 @@ defmodule File do
Note: The command `mv` in Unix-like systems behaves differently depending on
whether `source` is a file and the `destination` is an existing directory.
We have chosen to explicitly disallow this behavior.
We have chosen to explicitly disallow this behaviour.
## Examples
@@ -823,7 +822,7 @@ defmodule File do
The function receives arguments for `source_file` and `destination_file`. It should
return `true` if the existing file should be overwritten, `false` if otherwise.
The default callback returns `true`. On earlier versions, this callback could be
given as third argument, but such behavior is now deprecated.
given as third argument, but such behaviour is now deprecated.
"""
@spec cp(Path.t(), Path.t(), on_conflict: on_conflict_callback) :: :ok | {:error, posix}
@@ -896,7 +895,7 @@ defmodule File do
Note: The command `cp` in Unix-like systems behaves differently depending on
whether `destination` is an existing directory or not. We have chosen to
explicitly disallow this behavior. If `source` is a `file` and `destination`
explicitly disallow this behaviour. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned.
## Options
@@ -905,7 +904,7 @@ defmodule File do
The function receives arguments for `source` and `destination`. It should return
`true` if the existing file should be overwritten, `false` if otherwise. The default
callback returns `true`. On earlier versions, this callback could be given as third
argument, but such behavior is now deprecated.
argument, but such behaviour is now deprecated.
* `:dereference_symlinks` - (since v1.14.0) By default, this function will copy symlinks
by creating symlinks that point to the same location. This option forces symlinks to be
@@ -1364,11 +1363,6 @@ defmodule File do
{:ok, _} ->
{:ok, :regular}
{:error, :eio} when major == :win32 ->
# unix domain socket returns `{:error, :eio}`
# on other platforms the result is `{:ok, :regular}`
{:ok, :regular}
{:error, reason} ->
{:error, reason}
end
@@ -1395,6 +1389,13 @@ defmodule File do
@doc ~S"""
Opens the given `path`.
In order to write and read files, one must use the functions
in the `IO` module. By default, a file is opened in `:binary` mode,
which requires the functions `IO.binread/2` and `IO.binwrite/2`
to interact with the file. A developer may pass `:utf8` as an
option when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
`modes_or_function` can either be a list of modes or a function. If it's a
list, it's considered to be a list of modes (that are documented below). If
it's a function, then it's equivalent to calling `open(path, [],
@@ -1403,8 +1404,8 @@ defmodule File do
The allowed modes:
* `:binary` - opens the file in binary mode, disabling special handling of
Unicode sequences (default mode).
* `:binary` - opens the file in binary mode, disabling special handling of Unicode sequences
(default mode).
* `:read` - the file, which must exist, is opened for reading.
@@ -1445,33 +1446,19 @@ defmodule File do
This function returns:
* `{:ok, io_device | file_descriptor}` - the file has been opened in
the requested mode. We explore the differences between these two results
in the following section
* `{:ok, io_device}` - the file has been opened in the requested mode.
* `{:error, reason}` - the file could not be opened due to `reason`.
`io_device` is actually the PID of the process which handles the file.
This process monitors the process that originally opened the file (the
owner process). If the owner process terminates, the file is closed and
the process itself terminates too. If any process to which the `io_device`
is linked terminates, the file will be closed and the process itself will
be terminated.
## IO devices
An `io_device` returned from this call can be used as an argument to the
`IO` module functions.
By default, this function returns an IO device. An `io_device` is
a process which handles the file and you can interact with it using
the functions in the `IO` module. By default, a file is opened in
`:binary` mode, which requires the functions `IO.binread/2` and
`IO.binwrite/2` to interact with the file. A developer may pass `:utf8`
as a mode when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
Given the IO device is a file, if the owner process terminates,
the file is closed and the process itself terminates too. If any
process to which the `io_device` is linked terminates, the file
will be closed and the process itself will be terminated.
## File descriptors
When the `:raw` or `:ram` modes are given, this function returns
a low-level file descriptors. This avoids creating a process but
requires using the functions in the [`:file`](`:file`) module to
interact with it.
* `{:error, reason}` - the file could not be opened.
## Examples
@@ -1480,9 +1467,8 @@ defmodule File do
File.close(file)
"""
@spec open(Path.t(), [mode | :ram]) :: {:ok, io_device | file_descriptor} | {:error, posix}
@spec open(Path.t(), (io_device | file_descriptor -> res)) :: {:ok, res} | {:error, posix}
when res: var
@spec open(Path.t(), [mode | :ram]) :: {:ok, io_device} | {:error, posix}
@spec open(Path.t(), (io_device -> res)) :: {:ok, res} | {:error, posix} when res: var
def open(path, modes_or_function \\ [])
def open(path, modes) when is_list(modes) do
@@ -1516,8 +1502,7 @@ defmodule File do
See `open/2` for the list of available `modes`.
"""
@spec open(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) ::
{:ok, res} | {:error, posix}
@spec open(Path.t(), [mode | :ram], (io_device -> res)) :: {:ok, res} | {:error, posix}
when res: var
def open(path, modes, function) when is_list(modes) and is_function(function, 1) do
case open(path, modes) do
@@ -1539,8 +1524,8 @@ defmodule File do
See `open/2` for the list of available modes.
"""
@spec open!(Path.t(), [mode | :ram]) :: io_device | file_descriptor
@spec open!(Path.t(), (io_device | file_descriptor -> res)) :: res when res: var
@spec open!(Path.t(), [mode | :ram]) :: io_device
@spec open!(Path.t(), (io_device -> res)) :: res when res: var
def open!(path, modes_or_function \\ []) do
case open(path, modes_or_function) do
{:ok, io_device_or_function_result} ->
@@ -1559,7 +1544,7 @@ defmodule File do
See `open/2` for the list of available `modes`.
"""
@spec open!(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) :: res when res: var
@spec open!(Path.t(), [mode | :ram], (io_device -> res)) :: res when res: var
def open!(path, modes, function) do
case open(path, modes, function) do
{:ok, function_result} ->
+13 -13
View File
@@ -60,19 +60,19 @@ defmodule File.Stat do
defstruct keys
@type t :: %__MODULE__{
size: non_neg_integer() | :undefined,
type: :device | :directory | :regular | :other | :symlink | :undefined,
access: :read | :write | :read_write | :none | :undefined,
atime: :calendar.datetime() | integer() | :undefined,
mtime: :calendar.datetime() | integer() | :undefined,
ctime: :calendar.datetime() | integer() | :undefined,
mode: non_neg_integer() | :undefined,
links: non_neg_integer() | :undefined,
major_device: non_neg_integer() | :undefined,
minor_device: non_neg_integer() | :undefined,
inode: non_neg_integer() | :undefined,
uid: non_neg_integer() | :undefined,
gid: non_neg_integer() | :undefined
size: non_neg_integer(),
type: :device | :directory | :regular | :other | :symlink,
access: :read | :write | :read_write | :none,
atime: :calendar.datetime() | integer(),
mtime: :calendar.datetime() | integer(),
ctime: :calendar.datetime() | integer(),
mode: non_neg_integer(),
links: non_neg_integer(),
major_device: non_neg_integer(),
minor_device: non_neg_integer(),
inode: non_neg_integer(),
uid: non_neg_integer(),
gid: non_neg_integer()
}
@doc """
+10 -10
View File
@@ -210,7 +210,7 @@ defmodule Float do
## Known issues
The behavior of `floor/2` for floats can be surprising. For example:
The behaviour of `floor/2` for floats can be surprising. For example:
iex> Float.floor(12.52, 2)
12.51
@@ -218,7 +218,7 @@ defmodule Float do
One may have expected it to floor to 12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 12.51999999,
which explains the behavior above.
which explains the behaviour above.
## Examples
@@ -254,7 +254,7 @@ defmodule Float do
The operation is performed on the binary floating point, without a
conversion to decimal.
The behavior of `ceil/2` for floats can be surprising. For example:
The behaviour of `ceil/2` for floats can be surprising. For example:
iex> Float.ceil(-12.52, 2)
-12.51
@@ -262,7 +262,7 @@ defmodule Float do
One may have expected it to ceil to -12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as -12.51999999,
which explains the behavior above.
which explains the behaviour above.
This function always returns floats. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -307,7 +307,7 @@ defmodule Float do
## Known issues
The behavior of `round/2` for floats can be surprising. For example:
The behaviour of `round/2` for floats can be surprising. For example:
iex> Float.round(5.5675, 3)
5.567
@@ -315,8 +315,8 @@ defmodule Float do
One may have expected it to round to the half up 5.568. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance
which explains the behaviour above. If you want exact rounding for decimals,
you must use a decimal library. The behaviour above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
@@ -590,12 +590,12 @@ defmodule Float do
## Examples
iex> Float.to_charlist(7.0)
~c"7.0"
'7.0'
"""
@spec to_charlist(float) :: charlist
def to_charlist(float) when is_float(float) do
:erlang.float_to_list(float, [:short])
:io_lib_format.fwrite_g(float)
end
@doc """
@@ -622,7 +622,7 @@ defmodule Float do
"""
@spec to_string(float) :: String.t()
def to_string(float) when is_float(float) do
:erlang.float_to_binary(float, [:short])
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
end
@doc false
+1 -3
View File
@@ -22,7 +22,7 @@ defmodule GenEvent do
This approach has some shortcomings (it provides no back-pressure for example)
but can still replace GenEvent for low-profile usages of it. [This blog post
by José
Valim](https://dashbit.co/blog/replacing-genevent-by-a-supervisor-plus-genserver)
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
has more detailed information on this approach.
### GenStage
@@ -507,8 +507,6 @@ defmodule GenEvent do
{:ok, states, [name, handlers, hib]}
end
# Keeping deprecated format_status/2 since the current implementation is not
# compatible with format_status/1 and GenEvent is deprecated anyway
@doc false
def format_status(opt, status_data) do
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
+16 -30
View File
@@ -400,7 +400,7 @@ defmodule GenServer do
or is suspended, the parent PID, the debugger state, and the state of
the behaviour module, which includes the callback module state
(as returned by `:sys.get_state/2`). It's possible to change how this
status is represented by defining the optional `c:GenServer.format_status/1`
status is represented by defining the optional `c:GenServer.format_status/2`
callback.
* `:sys.trace/3` - prints all the system events to `:stdio`.
* `:sys.statistics/3` - manages collection of process statistics.
@@ -759,39 +759,22 @@ defmodule GenServer do
when old_vsn: term | {:down, term}
@doc """
This function is called by a `GenServer` process in the following situations:
Invoked in some cases to retrieve a formatted version of the `GenServer` status:
* [`:sys.get_status/1,2`](`:sys.get_status/1`) is invoked to get the `GenServer` status.
* The `GenServer` process terminates abnormally and logs an error.
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
status of the `GenServer`; in such cases, `reason` is `:normal`
This callback is used to limit the status of the process returned by
[`:sys.get_status/1,2`](`:sys.get_status/1`) or sent to logger.
* the `GenServer` terminates abnormally and logs an error; in such cases,
`reason` is `:terminate`
The callback gets a map `status` describing the current status and shall return
a map `new_status` with the same keys, but it may transform some values.
Two possible use cases for this callback is to remove sensitive information
from the state to prevent it from being printed in log files, or to compact
large irrelevant status items that would only clutter the logs.
## Example
@impl GenServer
def format_status(status) do
Map.new(status, fn
{:state, state} -> {:state, Map.delete(state, :private_key)}
{:message, {:password, _}} -> {:message, {:password, "redacted"}}
key_value -> key_value
end)
end
This callback can be useful to control the *appearance* of the status of the
`GenServer`. For example, it can be used to return a compact representation of
the `GenServer`'s state to avoid having large state terms printed.
`pdict_and_state` is a two-elements list `[pdict, state]` where `pdict` is a
list of `{key, value}` tuples representing the current process dictionary of
the `GenServer` and `state` is the current state of the `GenServer`.
"""
@doc since: "1.17.0"
@callback format_status(status :: :gen_server.format_status()) ::
new_status :: :gen_server.format_status()
# TODO: Remove this on v2.0
@doc deprecated: "Use format_status/1 callback instead"
@callback format_status(reason, pdict_and_state :: list) :: term
when reason: :normal | :terminate
@@ -800,7 +783,6 @@ defmodule GenServer do
handle_info: 2,
handle_cast: 2,
handle_call: 3,
format_status: 1,
format_status: 2,
handle_continue: 2
@@ -1120,6 +1102,10 @@ defmodule GenServer do
nil ->
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
# TODO: remove this clause when we require Erlang/OTP 25+
pid when pid == self() ->
exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}})
pid ->
try do
:gen.call(pid, :"$gen_call", request, timeout)
+5 -5
View File
@@ -258,13 +258,13 @@ end
defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
@@ -274,7 +274,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, module.size(dict)}
end
@@ -286,7 +286,7 @@ end
defimpl Collectable, for: HashDict do
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -303,7 +303,7 @@ defimpl Inspect, for: HashDict do
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end
end
+5 -5
View File
@@ -272,19 +272,19 @@ end
defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.size(set)}
end
@@ -296,7 +296,7 @@ end
defimpl Collectable, for: HashSet do
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
collector_fun = fn
set, {:cont, term} -> module.put(set, term)
@@ -313,7 +313,7 @@ defimpl Inspect, for: HashSet do
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end
end
+17 -31
View File
@@ -387,7 +387,7 @@ defimpl Inspect, for: Float do
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
[Integer.to_string(trunc(float)), ?., ?0]
else
:erlang.float_to_list(float, [:short])
:io_lib_format.fwrite_g(float)
end
color(IO.iodata_to_binary(formatted), :number, opts)
@@ -396,38 +396,24 @@ end
defimpl Inspect, for: Regex do
def inspect(regex = %{opts: regex_opts}, opts) when is_list(regex_opts) do
case translate_options(regex_opts, []) do
:error ->
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
translated_opts ->
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
color(source, :regex, opts)
end
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
end
defp translate_options([:dotall, {:newline, :anycrlf} | t], acc),
do: translate_options(t, [?s | acc])
def inspect(regex, opts) do
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
defp translate_options([:unicode, :ucp | t], acc), do: translate_options(t, [?u | acc])
defp translate_options([:caseless | t], acc), do: translate_options(t, [?i | acc])
defp translate_options([:extended | t], acc), do: translate_options(t, [?x | acc])
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, regex.opts])
color(source, :regex, opts)
end
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
@@ -463,7 +449,7 @@ defimpl Inspect, for: Function do
match?(@elixir_compiler ++ _, Atom.to_charlist(mod)) ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__()}>"
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
default_inspect(mod, fun_info)
end
-15
View File
@@ -588,21 +588,6 @@ defmodule Inspect.Algebra do
doc_cons(doc1, doc2)
end
@doc ~S"""
Disable any rendering limit while rendering the given document.
## Examples
iex> doc = Inspect.Algebra.glue("hello", "world") |> Inspect.Algebra.group()
iex> Inspect.Algebra.format(doc, 10)
["hello", "\n", "world"]
iex> doc = Inspect.Algebra.no_limit(doc)
iex> Inspect.Algebra.format(doc, 10)
["hello", " ", "world"]
"""
@doc since: "1.14.0"
@spec no_limit(t) :: t
def no_limit(doc) do
doc_limit(doc, :infinity)
end
+8 -8
View File
@@ -161,7 +161,7 @@ defmodule Integer do
@doc since: "1.4.0"
@spec floor_div(integer, neg_integer | pos_integer) :: integer
def floor_div(dividend, divisor) do
if :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
if dividend * divisor < 0 and rem(dividend, divisor) != 0 do
div(dividend, divisor) - 1
else
div(dividend, divisor)
@@ -362,25 +362,25 @@ defmodule Integer do
## Examples
iex> Integer.to_charlist(123)
~c"123"
'123'
iex> Integer.to_charlist(+456)
~c"456"
'456'
iex> Integer.to_charlist(-789)
~c"-789"
'-789'
iex> Integer.to_charlist(0123)
~c"123"
'123'
iex> Integer.to_charlist(100, 16)
~c"64"
'64'
iex> Integer.to_charlist(-100, 16)
~c"-64"
'-64'
iex> Integer.to_charlist(882_681_651, 36)
~c"ELIXIR"
'ELIXIR'
"""
@spec to_charlist(integer, 2..36) :: charlist
+10 -19
View File
@@ -128,15 +128,8 @@ defmodule IO do
@doc """
Reads from the IO `device`.
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes.
* if `line_or_chars` is `:line`, the device is iterated line by line.
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
The `device` is iterated by the given number of characters, line by line if
`:line` is given, or until `:eof`.
It returns:
@@ -152,10 +145,8 @@ defmodule IO do
@spec read(device, :eof | :line | non_neg_integer) :: chardata | nodata
def read(device \\ :stdio, line_or_chars)
# TODO: Remove me on v2.0
# TODO: Deprecate me on v1.17
def read(device, :all) do
IO.warn("IO.read(device, :all) is deprecated, use IO.read(device, :eof) instead")
with :eof <- read(device, :eof) do
with [_ | _] = opts <- :io.getopts(device),
false <- Keyword.get(opts, :binary, true) do
@@ -188,8 +179,10 @@ defmodule IO do
* if `line_or_chars` is `:line`, the device is iterated line by line.
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
* if `line_or_chars` is `:eof`, the device is iterated until `:eof`. `line_or_chars`
can only be `:eof` since Elixir 1.13.0. `:eof` replaces the deprecated `:all`,
with the difference that `:all` returns `""` on end of file, while `:eof` returns
`:eof` itself.
It returns:
@@ -207,9 +200,8 @@ defmodule IO do
@spec binread(device, :eof | :line | non_neg_integer) :: iodata | nodata
def binread(device \\ :stdio, line_or_chars)
# TODO: Remove me on v2.0
# TODO: Deprecate me on v1.17
def binread(device, :all) do
IO.warn("IO.binread(device, :all) is deprecated, use IO.binread(device, :eof) instead")
with :eof <- binread(device, :eof), do: ""
end
@@ -386,7 +378,7 @@ defmodule IO do
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
if :elixir_config.warn(key, stacktrace) do
warn(message.(), stacktrace)
warn(message, stacktrace)
else
:ok
end
@@ -416,7 +408,7 @@ defmodule IO do
end
@doc """
Inspects and writes the given `item` to the standard output.
Inspects and writes the given `item` to the device.
It's important to note that it returns the given `item` unchanged.
This makes it possible to "spy" on values by inserting an
@@ -432,7 +424,6 @@ defmodule IO do
The label will be printed before the inspected `item`.
See `Inspect.Opts` for a full list of remaining formatting options.
To print to other IO devices, see `IO.inspect/3`
## Examples
+2 -2
View File
@@ -268,7 +268,7 @@ defmodule IO.ANSI do
The named sequences are represented by atoms.
It will also append an `IO.ANSI.reset/0` to the chardata when a conversion is
performed. If you don't want this behavior, use `format_fragment/2`.
performed. If you don't want this behaviour, use `format_fragment/2`.
An optional boolean parameter can be passed to enable or disable
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
@@ -299,7 +299,7 @@ defmodule IO.ANSI do
## Examples
iex> IO.ANSI.format_fragment([:bright, ~c"Word"], true)
iex> IO.ANSI.format_fragment([:bright, 'Word'], true)
[[[[[[] | "\e[1m"], 87], 111], 114], 100]
"""
+8
View File
@@ -221,6 +221,10 @@ defmodule IO.ANSI.Docs do
### Quotes
defp process_quote([], lines, indent, options) do
write_quote(lines, indent, options, false)
end
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
write_quote(lines, indent, options, true)
write_empty_quote_line(options)
@@ -361,6 +365,10 @@ defmodule IO.ANSI.Docs do
### Code blocks
defp process_code([], code, indent, options) do
write_code(code, indent, options)
end
# Blank line between code blocks
defp process_code(["", " " <> line | rest], code, indent, options) do
process_code(rest, [line, "" | code], indent, options)
+5 -3
View File
@@ -1,9 +1,11 @@
defmodule IO.StreamError do
defexception [:reason]
defexception [:reason, :message]
@impl true
def message(%{reason: reason}) do
"error during streaming: #{inspect(reason)}"
def exception(opts) do
reason = opts[:reason]
formatted = IO.iodata_to_binary(:file.format_error(reason))
%IO.StreamError{message: "error during streaming: #{formatted}", reason: reason}
end
end
+146 -400
View File
@@ -125,7 +125,7 @@ defmodule Kernel do
### Supporting documents
Under the "Pages" section in sidebar you will find tutorials, guides,
and reference documents that outline Elixir semantics and behaviors
and reference documents that outline Elixir semantics and behaviours
in more detail. Those are:
* [Compatibility and deprecations](compatibility-and-deprecations.md) - lists
@@ -174,7 +174,10 @@ defmodule Kernel do
The functions in this module perform structural comparison. This allows
different data types to be compared using comparison operators:
1 < :an_atom
```elixir
iex> 1 < :an_atom
true
```
This is possible so Elixir developers can create collections, such as
dictionaries and ordered sets, that store a mixture of data types in them.
@@ -202,12 +205,9 @@ defmodule Kernel do
~D[2017-03-31] > ~D[2017-04-01]
will return `true` because structural comparison compares the `:day`
field before `:month` or `:year`. Luckily, the Elixir compiler will
detect whenever comparing structs or whenever comparing code that is
either always true or false, and emit a warning accordingly.
In order to perform semantic comparisons, the relevant data-types
provide a `compare/2` function, such as `Date.compare/2`:
field before `:month` or `:year`. In order to perform semantic comparisons,
the relevant data-types provide a `compare/2` function, such as
`Date.compare/2`:
iex> Date.compare(~D[2017-03-31], ~D[2017-04-01])
:lt
@@ -397,7 +397,7 @@ defmodule Kernel do
iex> binary_part("Hello", 5, -3)
"llo"
An `ArgumentError` is raised when the `size` is outside of the binary:
An `ArgumentError` is raised when the size is outside of the binary:
binary_part("Hello", 0, 10)
** (ArgumentError) argument error
@@ -405,8 +405,8 @@ defmodule Kernel do
"""
@doc guard: true
@spec binary_part(binary, non_neg_integer, integer) :: binary
def binary_part(binary, start, size) do
:erlang.binary_part(binary, start, size)
def binary_part(binary, start, length) do
:erlang.binary_part(binary, start, length)
end
@doc """
@@ -548,10 +548,10 @@ defmodule Kernel do
* `exit({:shutdown, term})`
Exiting with any other reason is considered abnormal and treated
as a crash. This means the default supervisor behavior kicks in,
as a crash. This means the default supervisor behaviour kicks in,
error reports are emitted, and so forth.
This behavior is relied on in many different places. For example,
This behaviour is relied on in many different places. For example,
`ExUnit` uses `exit(:shutdown)` when exiting the test process to
signal linked processes, supervision trees and so on to politely
shut down too.
@@ -636,26 +636,22 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is an atom, otherwise returns `false`.
Note `true`, `false`, and `nil` are atoms in Elixir, as well as
module names. Therefore this function will return `true` to all
of those values.
Returns `true` if `term` is an atom; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
## Examples
iex> is_atom(:name)
iex> is_atom(false)
true
iex> is_atom(false)
iex> is_atom(:name)
true
iex> is_atom(AnAtom)
true
iex> is_atom("string")
iex> is_atom("true")
false
"""
@@ -666,7 +662,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a binary, otherwise returns `false`.
Returns `true` if `term` is a binary; otherwise returns `false`.
A binary always contains a complete number of bytes.
@@ -687,7 +683,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a bitstring (including a binary), otherwise returns `false`.
Returns `true` if `term` is a bitstring (including a binary); otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
@@ -707,7 +703,7 @@ defmodule Kernel do
@doc """
Returns `true` if `term` is either the atom `true` or the atom `false` (i.e.,
a boolean), otherwise returns `false`.
a boolean); otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
@@ -730,7 +726,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a floating-point number, otherwise returns `false`.
Returns `true` if `term` is a floating-point number; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -741,7 +737,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a function, otherwise returns `false`.
Returns `true` if `term` is a function; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
@@ -781,7 +777,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is an integer, otherwise returns `false`.
Returns `true` if `term` is an integer; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -792,7 +788,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a list with zero or more elements, otherwise returns `false`.
Returns `true` if `term` is a list with zero or more elements; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -815,7 +811,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a PID (process identifier), otherwise returns `false`.
Returns `true` if `term` is a PID (process identifier); otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -826,7 +822,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a port identifier, otherwise returns `false`.
Returns `true` if `term` is a port identifier; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -837,7 +833,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a reference, otherwise returns `false`.
Returns `true` if `term` is a reference; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -848,7 +844,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a tuple, otherwise returns `false`.
Returns `true` if `term` is a tuple; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
"""
@@ -859,25 +855,9 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a map, otherwise returns `false`.
Returns `true` if `term` is a map; otherwise returns `false`.
Allowed in guard tests. Inlined by the compiler.
> #### Structs are maps {: .info}
>
> Structs are also maps, and many of Elixir data structures are implemented
> using structs: `Range`s, `Regex`es, `Date`s...
>
> iex> is_map(1..10)
> true
> iex> is_map(~D[2024-04-18])
> true
>
> If you mean to specifically check for non-struct maps, use
> `is_non_struct_map/1` instead.
>
> iex> is_non_struct_map(1..10)
> false
"""
@doc guard: true
@spec is_map(term) :: boolean
@@ -886,7 +866,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `key` is a key in `map`, otherwise returns `false`.
Returns `true` if `key` is a key in `map`; otherwise returns `false`.
It raises `BadMapError` if the first element is not a map.
@@ -971,7 +951,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Inlined by the compiler.
@@ -980,8 +960,8 @@ defmodule Kernel do
iex> max(1, 2)
2
iex> max("a", "b")
"b"
iex> max(:a, :b)
:b
"""
@spec max(first, second) :: first | second when first: term, second: term
@@ -997,7 +977,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Inlined by the compiler.
@@ -1106,9 +1086,6 @@ defmodule Kernel do
registered name, or a tuple in the form of `{registered_name, node}` for a
registered name at another node.
For additional documentation, see the [`!` operator Erlang
documentation](https://www.erlang.org/doc/reference_manual/expressions#send).
Inlined by the compiler.
## Examples
@@ -1548,7 +1525,6 @@ defmodule Kernel do
If the `right` operand is not a proper list, it returns an improper list.
If the `left` operand is not a proper list, it raises `ArgumentError`.
If the `left` operand is an empty list, it returns the `right` operand.
Inlined by the compiler.
@@ -1571,10 +1547,6 @@ defmodule Kernel do
iex> [1] ++ [2 | 3]
[1, 2 | 3]
# empty list on the left will return the right operand
iex> [] ++ 1
1
The `++/2` operator is right associative, meaning:
iex> [1, 2, 3] -- [1] ++ [2]
@@ -1586,8 +1558,7 @@ defmodule Kernel do
[3]
"""
@spec [] ++ a :: a when a: term()
@spec nonempty_list() ++ term() :: maybe_improper_list()
@spec list ++ term :: maybe_improper_list
def left ++ right do
:erlang.++(left, right)
end
@@ -1653,7 +1624,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1677,7 +1648,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1701,7 +1672,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1725,7 +1696,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1750,7 +1721,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1780,7 +1751,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1812,7 +1783,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1840,7 +1811,7 @@ defmodule Kernel do
This performs a structural comparison where all Elixir
terms can be compared with each other. See the ["Structural
comparison"](#module-structural-comparison) section
comparison" section](#module-structural-comparison) section
for more information.
Allowed in guard tests. Inlined by the compiler.
@@ -1912,7 +1883,7 @@ defmodule Kernel do
@doc """
Strictly boolean "or" operator.
If `left` is `true`, returns `true`, otherwise returns `right`.
If `left` is `true`, returns `true`; otherwise returns `right`.
Requires only the `left` operand to be a boolean since it short-circuits.
If the `left` operand is not a boolean, a `BadBooleanError` exception is
@@ -1944,7 +1915,7 @@ defmodule Kernel do
@doc """
Strictly boolean "and" operator.
If `left` is `false`, returns `false`, otherwise returns `right`.
If `left` is `false`, returns `false`; otherwise returns `right`.
Requires only the `left` operand to be a boolean since it short-circuits. If
the `left` operand is not a boolean, a `BadBooleanError` exception is raised.
@@ -2392,7 +2363,7 @@ defmodule Kernel do
Keys in the `Enumerable` that don't exist in the struct are automatically
discarded. Note that keys must be atoms, as only atoms are allowed when
defining a struct. If there are duplicate keys in the `Enumerable`, the last
entry will be taken (same behavior as `Map.new/1`).
entry will be taken (same behaviour as `Map.new/1`).
This function is useful for dynamically creating and updating structs, as
well as for converting maps to structs; in the latter case, just inserting
@@ -2440,7 +2411,7 @@ defmodule Kernel do
@doc """
Similar to `struct/2` but checks for key validity.
The function `struct!/2` emulates the compile time behavior
The function `struct!/2` emulates the compile time behaviour
of structs. This means that:
* when building a struct, as in `struct!(SomeStruct, key: :value)`,
@@ -2510,7 +2481,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a struct, otherwise returns `false`.
Returns `true` if `term` is a struct; otherwise returns `false`.
Allowed in guard tests.
@@ -2546,7 +2517,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a struct of `name`, otherwise returns `false`.
Returns `true` if `term` is a struct of `name`; otherwise returns `false`.
`is_struct/2` does not check that `name` exists and is a valid struct.
If you want such validations, you must pattern match on the struct
@@ -2594,49 +2565,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is a map that is not a struct, otherwise
returns `false`.
Allowed in guard tests.
## Examples
iex> is_non_struct_map(%{})
true
iex> is_non_struct_map(URI.parse("/"))
false
iex> is_non_struct_map(nil)
false
"""
@doc since: "1.17.0", guard: true
defmacro is_non_struct_map(term) do
case __CALLER__.context do
nil ->
quote do
case unquote(term) do
%_{} -> false
%{} -> true
_ -> false
end
end
:match ->
invalid_match!(:is_non_struct_map)
:guard ->
quote do
is_map(unquote(term)) and
not (:erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))))
end
end
end
@doc """
Returns `true` if `term` is an exception, otherwise returns `false`.
Returns `true` if `term` is an exception; otherwise returns `false`.
Allowed in guard tests.
@@ -2674,7 +2603,7 @@ defmodule Kernel do
end
@doc """
Returns `true` if `term` is an exception of `name`, otherwise returns `false`.
Returns `true` if `term` is an exception of `name`; otherwise returns `false`.
Allowed in guard tests.
@@ -2745,12 +2674,16 @@ defmodule Kernel do
end
@doc """
Gets a value from a nested structure with nil-safe handling.
Gets a value from a nested structure.
Uses the `Access` module to traverse the structures
according to the given `keys`, unless the `key` is a
function, which is detailed in a later section.
Note that if none of the given keys are functions,
there is rarely a reason to use `get_in` over
writing "regular" Elixir code using `[]`.
## Examples
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
@@ -2778,6 +2711,18 @@ defmodule Kernel do
iex> users["unknown"][:age]
nil
iex> users = nil
iex> get_in(users, [Access.all(), :age])
nil
Alternatively, if you need to access complex data-structures, you can
use pattern matching:
case users do
%{"john" => %{age: age}} -> age
_ -> default_value
end
## Functions as keys
If a key given to `get_in/2` is a function, the function will be invoked
@@ -2807,19 +2752,13 @@ defmodule Kernel do
get_in(some_struct, [:some_key, :nested_key])
There are two alternatives. Given structs have predefined keys,
we can use the `struct.field` notation:
The good news is that structs have predefined shape. Therefore,
you can write instead:
some_struct.some_key.nested_key
However, the code above will fail if any of the values return `nil`.
If you also want to handle nil values, you can use `get_in/1`:
get_in(some_struct.some_key.nested_key)
Pattern-matching is another option for handling such cases,
which can be especially useful if you want to match on several
fields at once or provide custom return values:
If, by any chance, `some_key` can return nil, you can always
fallback to pattern matching to provide nested struct handling:
case some_struct do
%{some_key: %{nested_key: value}} -> value
@@ -3037,63 +2976,6 @@ defmodule Kernel do
defp pop_in_data(data, [key | tail]),
do: Access.get_and_update(data, key, &pop_in_data(&1, tail))
@doc """
Gets a key from the nested structure via the given `path`, with
nil-safe handling.
This is similar to `get_in/2`, except the path is extracted via
a macro rather than passing a list. For example:
get_in(opts[:foo][:bar])
Is equivalent to:
get_in(opts, [:foo, :bar])
Additionally, this macro can traverse structs:
get_in(struct.foo.bar)
In case any of the keys returns `nil`, then `nil` will be returned
and `get_in/1` won't traverse any further.
Note that in order for this macro to work, the complete path must always
be visible by this macro. For more information about the supported path
expressions, please check `get_and_update_in/2` docs.
## Examples
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users["john"].age)
27
iex> get_in(users["unknown"].age)
nil
"""
defmacro get_in(path) do
{[h | t], _} = unnest(path, [], true, "get_in/1")
nest_get_in(h, quote(do: x), t)
end
defp nest_get_in(h, _var, []) do
h
end
defp nest_get_in(h, var, [{:map, key} | tail]) do
quote generated: true do
case unquote(h) do
%{unquote(key) => unquote(var)} -> unquote(nest_get_in(var, var, tail))
nil -> nil
unquote(var) -> :erlang.error({:badkey, unquote(key), unquote(var)})
end
end
end
defp nest_get_in(h, var, [{:access, key} | tail]) do
h = quote do: Access.get(unquote(h), unquote(key))
nest_get_in(h, var, tail)
end
@doc """
Puts a value in a nested structure via the given `path`.
@@ -3129,7 +3011,7 @@ defmodule Kernel do
defmacro put_in(path, value) do
case unnest(path, [], true, "put_in/2") do
{[h | t], true} ->
nest_map_update_in(h, t, quote(do: fn _ -> unquote(value) end))
nest_update_in(h, t, quote(do: fn _ -> unquote(value) end))
{[h | t], false} ->
expr = nest_get_and_update_in(h, t, quote(do: fn _ -> {nil, unquote(value)} end))
@@ -3206,7 +3088,7 @@ defmodule Kernel do
defmacro update_in(path, fun) do
case unnest(path, [], true, "update_in/2") do
{[h | t], true} ->
nest_map_update_in(h, t, fun)
nest_update_in(h, t, fun)
{[h | t], false} ->
expr = nest_get_and_update_in(h, t, quote(do: fn x -> {nil, unquote(fun).(x)} end))
@@ -3272,17 +3154,17 @@ defmodule Kernel do
nest_get_and_update_in(h, t, fun)
end
defp nest_map_update_in([], fun), do: fun
defp nest_update_in([], fun), do: fun
defp nest_map_update_in(list, fun) do
defp nest_update_in(list, fun) do
quote do
fn x -> unquote(nest_map_update_in(quote(do: x), list, fun)) end
fn x -> unquote(nest_update_in(quote(do: x), list, fun)) end
end
end
defp nest_map_update_in(h, [{:map, key} | t], fun) do
defp nest_update_in(h, [{:map, key} | t], fun) do
quote do
Map.update!(unquote(h), unquote(key), unquote(nest_map_update_in(t, fun)))
Map.update!(unquote(h), unquote(key), unquote(nest_update_in(t, fun)))
end
end
@@ -3508,7 +3390,7 @@ defmodule Kernel do
The pin operator will check if the values are equal, using `===/2`, while
patterns have their own rules when matching maps, lists, and so forth.
Such behavior is not specific to `match?/2`. The following code also
Such behaviour is not specific to `match?/2`. The following code also
throws an exception:
attrs = %{x: 1}
@@ -3612,6 +3494,37 @@ defmodule Kernel do
def example1, do: files()[:example1]
def example2, do: files()[:example2]
## Attention! Compile-time dependencies
Keep in mind references to other modules, even in module attributes,
generate compile-time dependencies to said modules.
For example, take this common pattern:
@values [:foo, :bar, :baz]
def handle_arg(arg) when arg in @values do
...
end
While the above is fine, imagine if instead you have actual
module names in the module attribute, like this:
@values [Foo, Bar, Baz]
def handle_arg(arg) when arg in @values do
...
end
The code above will define a compile-time dependency on the modules
`Foo`, `Bar`, and `Baz`, in a way that, if any of them change, the
current module will have to recompile. In such cases, it may be
preferred to avoid the module attribute altogether:
def handle_arg(arg) when arg in [Foo, Bar, Baz] do
...
end
"""
defmacro @expr
@@ -3865,14 +3778,7 @@ defmodule Kernel do
Provides an `if/2` macro.
This macro expects the first argument to be a condition and the second
argument to be a keyword list. Generally speaking, Elixir developers
prefer to use pattern matching and guards in function definitions and
`case/2`, as they are succinct and precise. However, not all conditions
can be expressed through patterns and guards, which makes `if/2` a viable
alternative.
Similar to `case/2`, any assignment in the condition will be available
on both clauses, as well as after the `if` expression.
argument to be a keyword list.
## One-liner examples
@@ -3904,8 +3810,7 @@ defmodule Kernel do
baz
end
If you find yourself nesting conditionals inside conditionals,
consider using `cond/1`.
In order to compare more than two clauses, the `cond/1` macro has to be used.
"""
defmacro if(condition, clauses) do
build_if(condition, clauses)
@@ -4022,7 +3927,7 @@ defmodule Kernel do
one element, which is the number itself.
If first is more than last, the range will be decreasing from first
to last, albeit this behavior is deprecated. Instead prefer to
to last, albeit this behaviour is deprecated. Instead prefer to
explicitly list the step with `first..last//-1`.
See the `Range` module for more information.
@@ -4044,36 +3949,24 @@ defmodule Kernel do
first = Macro.expand(first, __CALLER__)
last = Macro.expand(last, __CALLER__)
validate_range!(first, last)
stepless_range(__CALLER__.context, first, last, __CALLER__)
range(__CALLER__.context, first, last)
false ->
stepless_range(__CALLER__.context, first, last, __CALLER__)
range(__CALLER__.context, first, last)
end
end
defp stepless_range(_context, first, last, caller)
when is_integer(first) and is_integer(last) do
step =
if first <= last do
1
else
# TODO: Always use 1 as an step in Elixir v2.0
IO.warn(
"#{first}..#{last} has a default step of -1, please write #{first}..#{last}//-1 instead",
Macro.Env.stacktrace(caller)
)
-1
end
defp range(_context, first, last) when is_integer(first) and is_integer(last) do
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.17
step = if first <= last, do: 1, else: -1
{:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]}
end
defp stepless_range(nil, first, last, _caller) do
defp range(nil, first, last) do
quote(do: Elixir.Range.new(unquote(first), unquote(last)))
end
defp stepless_range(:guard, first, last, caller) do
defp range(:guard, first, last) do
# We need to compute the step using guards. We don't have conditionals,
# but we can emulate them using a map access.
step =
@@ -4084,27 +3977,12 @@ defmodule Kernel do
)
end
# TODO: Always assume step 1 in Elixir v2.0
range = "#{Macro.to_string(first)}..#{Macro.to_string(last)}"
IO.warn(
"#{range} inside guards requires an explicit step, please write #{range}//1 or #{range}//-1 instead",
Macro.Env.stacktrace(caller)
)
# TODO: Deprecate me inside guard when sides are not integers on Elixir v1.17
{:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]}
end
defp stepless_range(:match, first, last, caller) do
# TODO: Make me an error in Elixir v2.0
range = "#{Macro.to_string(first)}..#{Macro.to_string(last)}"
IO.warn(
"#{range} inside match is deprecated, " <>
"you must always match on the step: #{range}//var or #{range}//_ if you want to ignore it",
Macro.Env.stacktrace(caller)
)
defp range(:match, first, last) do
# TODO: Deprecate me inside match in all occasions (including literals) on Elixir v1.17
{:%{}, [], [__struct__: Elixir.Range, first: first, last: last]}
end
@@ -5058,15 +4936,17 @@ defmodule Kernel do
defp alias_meta(_), do: []
# We don't want to trace :alias_reference since we are defining the alias
defp expand_module_alias({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp expand_module_alias({:__aliases__, _, _} = original, env) do
case :elixir_aliases.expand_or_concat(original, env) do
receiver when is_atom(receiver) ->
receiver
[head | tail] ->
case Macro.expand(head, env) do
head when is_atom(head) -> :elixir_aliases.concat([head | tail])
_ -> alias
aliases ->
aliases = :lists.map(&Macro.expand(&1, env), aliases)
case :lists.all(&is_atom/1, aliases) do
true -> :elixir_aliases.concat(aliases)
false -> original
end
end
end
@@ -5542,8 +5422,8 @@ defmodule Kernel do
end
# Calls to Kernel functions must be fully-qualified to ensure
# reproducible builds, otherwise, this macro will generate ASTs
# with different metadata (:imports, :context) depending on if
# reproducible builds; otherwise, this macro will generate ASTs
# with different metadata (:import, :context) depending on if
# it is the bootstrapped version or not.
Elixir.Kernel.@(impl(true))
@@ -6133,156 +6013,14 @@ defmodule Kernel do
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end
hour_in_ms = 1000 * 60 * 60
day_in_ms = 24 * hour_in_ms
week_in_ms = 7 * day_in_ms
@doc """
Constructs a millisecond timeout from the given components, duration, or timeout.
This function is useful for constructing timeouts to use in functions that
expect `t:timeout/0` values (such as `Process.send_after/4` and many others).
## Argument
The `duration` argument can be one of a `Duration`, a `t:timeout/0`, or a list
of components. Each of these is described below.
### Passing `Duration`s
`t:Duration.t/0` structs can be converted to timeouts. The given duration must have
`year` and `month` fields set to `0`, since those cannot be reliably converted to
milliseconds (due to the varying number of days in a month and year).
Microseconds in durations are converted to milliseconds (through `System.convert_time_unit/3`).
### Passing components
The `duration` argument can also be keyword list which can contain the following
keys, each appearing at most once with a non-negative integer value:
* `:week` - the number of weeks (a week is always 7 days)
* `:day` - the number of days (a day is always 24 hours)
* `:hour` - the number of hours
* `:minute` - the number of minutes
* `:second` - the number of seconds
* `:millisecond` - the number of milliseconds
The timeout is calculated as the sum of the components, each multiplied by
the corresponding factor.
### Passing timeouts
You can also pass timeouts directly to this functions, that is, milliseconds or
the atom `:infinity`. In this case, this function just returns the given argument.
## Examples
With a keyword list:
iex> to_timeout(hour: 1, minute: 30)
5400000
With a duration:
iex> to_timeout(%Duration{hour: 1, minute: 30})
5400000
With a timeout:
iex> to_timeout(5400000)
5400000
iex> to_timeout(:infinity)
:infinity
"""
@doc since: "1.17.0"
@spec to_timeout([{unit, non_neg_integer()}] | timeout() | Duration.t()) :: timeout()
when unit: :week | :day | :hour | :minute | :second | :millisecond
def to_timeout(duration)
def to_timeout(:infinity), do: :infinity
def to_timeout(timeout) when is_integer(timeout) and timeout >= 0, do: timeout
def to_timeout(%{__struct__: Duration} = duration) do
case duration do
%{year: year} when year != 0 ->
raise ArgumentError,
"duration with a non-zero year cannot be reliably converted to timeouts"
%{month: month} when month != 0 ->
raise ArgumentError,
"duration with a non-zero month cannot be reliably converted to timeouts"
_other ->
{microsecond, _precision} = duration.microsecond
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
end
end
def to_timeout(components) when is_list(components) do
reducer = fn
{key, value}, {acc, seen_keys} when is_integer(value) and value >= 0 ->
case :lists.member(key, seen_keys) do
true ->
raise ArgumentError, "timeout component #{inspect(key)} is duplicated"
false ->
:ok
end
factor =
case key do
:week ->
unquote(week_in_ms)
:day ->
unquote(day_in_ms)
:hour ->
unquote(hour_in_ms)
:minute ->
60_000
:second ->
1000
:millisecond ->
1
other ->
raise ArgumentError, """
timeout component #{inspect(other)} is not a valid timeout component, valid \
values are: :week, :day, :hour, :minute, :second, :millisecond\
"""
end
{acc + value * factor, [key | seen_keys]}
{key, value}, {_acc, _seen_keys} ->
raise ArgumentError,
"timeout component #{inspect(key)} must be a non-negative " <>
"integer, got: #{inspect(value)}"
end
elem(:lists.foldl(reducer, {0, _seen_keys = []}, components), 0)
end
## Sigils
@doc ~S"""
Handles the sigil `~S` for strings.
It returns a string without interpolations and without escape
characters.
characters, except for the escaping of the closing sigil character
itself.
## Examples
@@ -6293,6 +6031,12 @@ defmodule Kernel do
iex> ~S(\o/)
"\\o/"
However, if you want to re-use the sigil character itself on
the string, you need to escape it:
iex> ~S((\))
"()"
"""
defmacro sigil_S(term, modifiers)
defmacro sigil_S({:<<>>, _, [binary]}, []) when is_binary(binary), do: binary
@@ -6329,7 +6073,8 @@ defmodule Kernel do
Handles the sigil `~C` for charlists.
It returns a charlist without interpolations and without escape
characters.
characters, except for the escaping of the closing sigil character
itself.
A charlist is a list of integers where all the integers are valid code points.
The three expressions below are equivalent:
@@ -6739,7 +6484,8 @@ defmodule Kernel do
Handles the sigil `~W` for list of words.
It returns a list of "words" split by whitespace without interpolations
and without escape characters.
and without escape characters, except for the escaping of the closing
sigil character itself.
## Modifiers
+2 -18
View File
@@ -327,8 +327,6 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | profile: :time})
end
## IEx
defp parse_argv([~c"--dbg", backend | t], %{mode: :iex} = config) do
case backend do
~c"pry" ->
@@ -346,21 +344,6 @@ defmodule Kernel.CLI do
defp parse_argv([~c"--dot-iex", _ | t], %{mode: :iex} = config), do: parse_argv(t, config)
defp parse_argv([~c"--remsh", _ | t], %{mode: :iex} = config), do: parse_argv(t, config)
## Erlang flags
defp parse_argv([~c"--boot", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--boot-var", _, _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--cookie", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--hidden" | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--erl-config", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--logger-otp-reports", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--logger-sasl-reports", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--name", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--sname", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--vm-args", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--erl", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--pipe-to", _, _ | t], config), do: parse_argv(t, config)
## Fallback
defp parse_argv([h | t], %{mode: :elixirc} = config) do
@@ -479,7 +462,8 @@ defmodule Kernel.CLI do
defp process_command({:compile, patterns}, config) do
# If ensuring the dir returns an error no files will be found.
_ = :filelib.ensure_path(config.output)
# TODO: Use :filelib.ensure_path/1 once we require Erlang/OTP 25+
_ = :filelib.ensure_dir(:filename.join(config.output, "."))
case filter_multiple_patterns(patterns) do
{:ok, []} ->
+30 -36
View File
@@ -18,36 +18,23 @@ defmodule Kernel.ParallelCompiler do
# TODO: Deprecate this on Elixir v1.20.
@doc deprecated: "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun)
send(task.pid, ref)
task
end
defp inner_async(fun) do
case :erlang.get(:elixir_compiler_info) do
{compiler_pid, file_pid} ->
ref = make_ref()
file = :erlang.get(:elixir_compiler_file)
dest = :erlang.get(:elixir_compiler_dest)
{:error_handler, error_handler} = :erlang.process_info(self(), :error_handler)
{_parent, checker} = Module.ParallelChecker.get()
task =
Task.async(fn ->
Module.ParallelChecker.put(compiler_pid, checker)
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
:erlang.process_flag(:error_handler, error_handler)
receive do
^ref -> fun.()
end
end)
send(compiler_pid, {:async, task.pid})
{ref, task}
Task.async(fn ->
send(compiler_pid, {:async, self()})
Module.ParallelChecker.put(compiler_pid, checker)
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
:erlang.process_flag(:error_handler, error_handler)
fun.()
end)
:undefined ->
raise ArgumentError,
@@ -65,29 +52,41 @@ defmodule Kernel.ParallelCompiler do
"""
@doc since: "1.16.0"
def pmap(collection, fun) when is_function(fun, 1) do
parent = self()
ref = make_ref()
# We spawn a series of tasks for parallel processing.
# The tasks are waiting until we give the go ahead.
refs_tasks =
# The tasks notify themselves to the compiler.
tasks =
Enum.map(collection, fn item ->
inner_async(fn -> fun.(item) end)
async(fn ->
send(parent, {ref, self()})
receive do
^ref -> fun.(item)
end
end)
end)
# Then the tasks notify us. This is important because if
# we wait before the tasks notify the compiler, we may be
# released as there is nothing else running.
on =
for %{pid: pid} <- tasks do
receive do
{^ref, ^pid} -> pid
end
end
# Notify the compiler we are waiting on the tasks.
{compiler_pid, file_pid} = :erlang.get(:elixir_compiler_info)
defining = :elixir_module.compiler_modules()
on = Enum.map(refs_tasks, fn {_ref, %{pid: pid}} -> pid end)
send(compiler_pid, {:waiting, :pmap, self(), ref, file_pid, on, defining, :raise})
# Now we allow the tasks to run. This step is not strictly
# necessary but it makes compilation more deterministic by
# only allowing tasks to run once we are waiting.
tasks =
Enum.map(refs_tasks, fn {ref, task} ->
send(task.pid, ref)
task
end)
Enum.each(on, &send(&1, ref))
# Await tasks and notify the compiler they are done. We could
# have the tasks report directly to the compiler, which in turn
@@ -467,11 +466,9 @@ defmodule Kernel.ParallelCompiler do
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, [], extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} ->
@@ -743,9 +740,6 @@ defmodule Kernel.ParallelCompiler do
end
defp return_error(warnings, errors, state, fun) do
# Also prune compiler modules in case of errors
:elixir_code_server.cast(:purge_compiler_modules)
errors =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
+17 -44
View File
@@ -550,7 +550,7 @@ defmodule Kernel.SpecialForms do
Elixir won't emit any warnings though, since the alias
was not explicitly defined.
Both warning behaviors could be changed by explicitly
Both warning behaviours could be changed by explicitly
setting the `:warn` option to `true` or `false`.
"""
@@ -674,7 +674,7 @@ defmodule Kernel.SpecialForms do
Elixir won't emit any warnings though, since the import
was not explicitly defined.
Both warning behaviors could be changed by explicitly
Both warning behaviours could be changed by explicitly
setting the `:warn` option to `true` or `false`.
## Ambiguous function/macro names
@@ -888,7 +888,7 @@ defmodule Kernel.SpecialForms do
At first, there is nothing in this example that actually reveals it is a
macro. But what is happening is that, at compilation time, `squared(5)`
becomes `5 * 5`. The argument `5` is duplicated in the produced code, we
can see this behavior in practice though because our macro actually has
can see this behaviour in practice though because our macro actually has
a bug:
import Math
@@ -915,7 +915,7 @@ defmodule Kernel.SpecialForms do
my_number.() * my_number.()
Which invokes the function twice, explaining why we get the printed value
twice! In the majority of the cases, this is actually unexpected behavior,
twice! In the majority of the cases, this is actually unexpected behaviour,
and that's why one of the first things you need to keep in mind when it
comes to macros is to **not unquote the same value more than once**.
@@ -1444,7 +1444,7 @@ defmodule Kernel.SpecialForms do
[elixir: :prolog]
Given the grandparents of Erlang and Prolog were nil, those values were
filtered out. If you don't want this behavior, a simple option is to
filtered out. If you don't want this behaviour, a simple option is to
move the filter inside the do-block:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
@@ -1494,9 +1494,7 @@ defmodule Kernel.SpecialForms do
## The `:reduce` option
*Available since Elixir v1.8*.
While the `:into` option allows us to customize the comprehension behavior
While the `:into` option allows us to customize the comprehension behaviour
to a given data type, such as putting all of the values inside a map or inside
a binary, it is not always enough.
@@ -1624,7 +1622,7 @@ defmodule Kernel.SpecialForms do
iex> width
nil
The behavior of any expression in a clause is the same as if it was
The behaviour of any expression in a clause is the same as if it was
written outside of `with`. For example, `=` will raise a `MatchError`
instead of returning the non-matched value:
@@ -1756,15 +1754,6 @@ defmodule Kernel.SpecialForms do
"""
defmacro unquote(:__block__)(args), do: error!([args])
@doc """
Internal special form for cursor position.
This is the special form used whenever we need to represent
the cursor position in Elixir's AST. See `Code.Fragment` for
more information.
"""
defmacro unquote(:__cursor__)(args), do: error!([args])
@doc """
Capture operator. Captures or creates an anonymous function.
@@ -1887,11 +1876,6 @@ defmodule Kernel.SpecialForms do
@doc ~S"""
Matches the given expression against the given clauses.
`case/2` relies on pattern matching and guards to choose
which clause to execute. If your logic cannot be expressed
within patterns and guards, consider using `if/2` or `cond/1`
instead.
## Examples
case File.read(file) do
@@ -1922,9 +1906,6 @@ defmodule Kernel.SpecialForms do
end
#=> "This clause would match any value (x = 10)"
If you find yourself nesting `case` expressions inside
`case` expressions, consider using `with/1`.
## Variable handling
Note that variables bound in a clause do not leak to the outer context:
@@ -1984,20 +1965,17 @@ defmodule Kernel.SpecialForms do
Evaluates the expression corresponding to the first clause that
evaluates to a truthy value.
## Examples
The following example has a single clause that always evaluates
to true:
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
If all clauses evaluate to `nil` or `false`, `cond` raises an error.
Raises an error if all conditions evaluate to `nil` or `false`.
For this reason, it may be necessary to add a final always-truthy condition
(anything non-`false` and non-`nil`), which will always match:
(anything non-`false` and non-`nil`), which will always match.
## Examples
cond do
1 + 1 == 1 ->
@@ -2009,9 +1987,6 @@ defmodule Kernel.SpecialForms do
end
#=> "This will"
If your `cond` has two clauses, and the last one falls back to
`true`, you may consider using `if/2` instead.
"""
defmacro cond(clauses), do: error!([clauses])
@@ -2325,13 +2300,11 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args])
@doc """
Checks if there is a message matching any of the given clauses in the current
process mailbox.
Checks if there is a message matching the given clauses
in the current process mailbox.
If there is no matching message, the current process waits until a matching
message arrives or until after a given timeout value.
Any new and existing messages that do not match will remain in the mailbox.
In case there is no such message, the current process hangs
until a message arrives or waits until a given timeout value.
## Examples
@@ -2344,8 +2317,8 @@ defmodule Kernel.SpecialForms do
IO.puts(:stderr, "Unexpected message received")
end
An optional `after` clause can be given in case no matching message is
received during the given timeout period, specified in milliseconds:
An optional `after` clause can be given in case the message was not
received after the given timeout period, specified in milliseconds:
receive do
{:selector, number, name} when is_integer(number) ->
+10 -8
View File
@@ -917,11 +917,11 @@ defmodule Kernel.Typespec do
typespec({nil, [], []}, vars, caller, state)
end
defp typespec([{:..., _, _}], vars, caller, state) do
defp typespec([{:..., _, atom}], vars, caller, state) when is_atom(atom) do
typespec({:nonempty_list, [], []}, vars, caller, state)
end
defp typespec([spec, {:..., _, _}], vars, caller, state) do
defp typespec([spec, {:..., _, atom}], vars, caller, state) when is_atom(atom) do
typespec({:nonempty_list, [], [spec]}, vars, caller, state)
end
@@ -950,15 +950,17 @@ defmodule Kernel.Typespec do
# This is a backport of Macro.expand/2 because we want to expand
# aliases but we don't them to become compile-time references.
defp expand_remote({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp expand_remote({:__aliases__, _, _} = alias, env) do
case :elixir_aliases.expand(alias, env) do
receiver when is_atom(receiver) ->
receiver
[head | tail] ->
case Macro.expand_once(head, env) do
head when is_atom(head) -> :elixir_aliases.concat([head | tail])
_ -> alias
aliases ->
aliases = :lists.map(&Macro.expand_once(&1, env), aliases)
case :lists.all(&is_atom/1, aliases) do
true -> :elixir_aliases.concat(aliases)
false -> alias
end
end
end
+4 -4
View File
@@ -120,7 +120,7 @@ defmodule Kernel.Utils do
mapper = fn
{key, val} when is_atom(key) ->
try do
:elixir_quote.escape(val, :none, false)
:elixir_quote.escape(val, false, :none)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
@@ -208,10 +208,10 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
# The __struct__ attribute is during expansion and for loading remote structs
# The __struct__ field is used for expansion and for loading remote structs
:ets.insert(set, {:__struct__, struct, nil, []})
# The complete metadata goes into __info__(:struct)
# Store all field metadata to go into __info__(:struct)
mapper = fn {key, val} ->
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
@@ -289,7 +289,7 @@ defmodule Kernel.Utils do
macro.
Secondly, if the expression is being used outside of a guard, we want to unquote
`value`, but only once, and then reuse the unquoted form throughout the expression.
`value`, but only once, and then re-use the unquoted form throughout the expression.
This helper does exactly that: takes the AST for an expression and a list of
variable references it should be aware of, and rewrites it into a new expression
+3 -36
View File
@@ -102,9 +102,6 @@ defmodule Keyword do
@type key :: atom
@type value :: any
@typedoc since: "1.17.0"
@type default :: any
@type t :: [{key, value}]
@type t(value) :: [{key, value}]
@@ -391,7 +388,7 @@ defmodule Keyword do
3
"""
@spec get(t, key, default) :: value | default
@spec get(t, key, value) :: value
def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, value} -> value
@@ -960,36 +957,6 @@ defmodule Keyword do
:lists.sort(left) === :lists.sort(right)
end
@doc """
Intersects two keyword lists, returning a keyword with the common keys.
By default, it returns the values of the intersected keys in `keyword2`.
The keys are returned in the order found in `keyword1`.
## Examples
iex> Keyword.intersect([a: 1, b: 2], [b: "b", c: "c"])
[b: "b"]
iex> Keyword.intersect([a: 1, b: 2], [b: 2, c: 3], fn _k, v1, v2 ->
...> v1 + v2
...> end)
[b: 4]
"""
@doc since: "1.17.0"
@spec intersect(keyword, keyword, (key, value, value -> value)) :: keyword
def intersect(keyword1, keyword2, fun \\ fn _key, _v1, v2 -> v2 end)
def intersect([{k, v1} | keyword1], keyword2, fun) do
case :lists.keyfind(k, 1, keyword2) do
{_, v2} -> [{k, fun.(k, v1, v2)} | intersect(keyword1, keyword2, fun)]
false -> intersect(keyword1, keyword2, fun)
end
end
def intersect([], _keyword2, _fun), do: []
@doc """
Merges two keyword lists into one.
@@ -1322,7 +1289,7 @@ defmodule Keyword do
{1, []}
"""
@spec pop(t, key, default) :: {value | default, t}
@spec pop(t, key, value) :: {value, t}
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case fetch(keywords, key) do
{:ok, value} -> {value, delete(keywords, key)}
@@ -1438,7 +1405,7 @@ defmodule Keyword do
{1, [a: 2]}
"""
@spec pop_first(t, key, default) :: {value | default, t}
@spec pop_first(t, key, value) :: {value, t}
def pop_first(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keytake(key, 1, keywords) do
{:value, {^key, value}, rest} -> {value, rest}
+2 -2
View File
@@ -107,7 +107,7 @@ defmodule List do
charlists in IEx when you encounter them, which shows you the type, description
and also the raw representation in one single summary.
The rationale behind this behavior is to better support
The rationale behind this behaviour is to better support
Erlang libraries which may return text as charlists
instead of Elixir strings. In Erlang, charlists are the default
way of handling strings, while in Elixir it's binaries. One
@@ -895,7 +895,7 @@ defmodule List do
end
@doc """
Returns `true` if `list` starts with the given `prefix` list, otherwise returns `false`.
Returns `true` if `list` starts with the given `prefix` list; otherwise returns `false`.
If `prefix` is an empty list, it returns `true`.
+1 -1
View File
@@ -58,6 +58,6 @@ end
defimpl List.Chars, for: Float do
def to_charlist(term) do
:erlang.float_to_list(term, [:short])
:io_lib_format.fwrite_g(term)
end
end
+67 -44
View File
@@ -156,10 +156,9 @@ defmodule Macro do
* `:end_of_expression` - denotes when the end of expression effectively
happens (when `:token_metadata` is true). This is only available for
expressions inside "blocks of code", which are either direct children
of a `__block__` or the right side of `->`. The last expression of the
block does not have metadata if it is not followed by an end of line
character (either a newline or `;`)
direct children of a `__block__`, and it is either the location of a
newline or of the `;` character. The last expression of `__block__`
does not have this metadata.
* `:indentation` - indentation of a sigil heredoc
@@ -371,7 +370,7 @@ defmodule Macro do
Note the arguments are not unique. If you later on want
to access the same variables, you can invoke this function
with the same inputs. Use `generate_unique_arguments/2` to
generate unique arguments that can't be overridden.
generate a unique arguments that can't be overridden.
## Examples
@@ -495,8 +494,10 @@ defmodule Macro do
"""
@doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) ::
[{atom, [counter: integer], context}, ...]
@spec generate_unique_arguments(pos_integer, context) :: [
{atom, [counter: integer], context},
...
]
when context: atom
def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2)
@@ -1132,6 +1133,7 @@ defmodule Macro do
"""
@spec to_string(t()) :: String.t()
# TODO: Allow line_length to be configurable on v1.17
def to_string(tree) do
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
IO.iodata_to_binary(doc)
@@ -1711,7 +1713,7 @@ defmodule Macro do
@doc """
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
This is used when you want to programmatically invoke a macro at
This is used when you want to programatically invoke a macro at
compile-time.
"""
@doc since: "1.16.0"
@@ -1732,9 +1734,7 @@ defmodule Macro do
If the expression cannot be expanded, it returns the expression
itself. This function does not traverse the AST, only the root
node is expanded. The expansion happens as if it was expanded by
the Elixir compiler and therefore compilation tracers will be invoked
and deprecation warnings will be emitted during the expansion.
node is expanded.
`expand_once/2` performs the expansion just once. Check `expand/2`
to perform expansion until the node can no longer be expanded.
@@ -1805,23 +1805,23 @@ defmodule Macro do
elem(do_expand_once(ast, env), 0)
end
defp do_expand_once({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp do_expand_once({:__aliases__, meta, _} = original, env) do
case :elixir_aliases.expand_or_concat(original, env) do
receiver when is_atom(receiver) ->
:elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
[head | tail] ->
{head, _} = do_expand_once(head, env)
aliases ->
aliases = :lists.map(&elem(do_expand_once(&1, env), 0), aliases)
case is_atom(head) do
case :lists.all(&is_atom/1, aliases) do
true ->
receiver = :elixir_aliases.concat([head | tail])
receiver = :elixir_aliases.concat(aliases)
:elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
false ->
{alias, false}
{original, false}
end
end
end
@@ -1832,13 +1832,13 @@ defmodule Macro do
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
do: {:filename.dirname(env.file), true}
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
env = update_in(env.versioned_vars, &maybe_escape_map/1)
{maybe_escape_map(env), true}
end
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
when is_atom(atom) and is_atom(field) and env.context != :match do
when is_atom(atom) and is_atom(field) do
if Map.has_key?(env, field) do
{maybe_escape_map(Map.get(env, field)), true}
else
@@ -1846,39 +1846,56 @@ defmodule Macro do
end
end
defp do_expand_once({name, meta, context} = original, _env)
when is_atom(name) and is_list(meta) and is_atom(context) do
defp do_expand_once({atom, meta, context} = original, _env)
when is_atom(atom) and is_list(meta) and is_atom(context) do
{original, false}
end
defp do_expand_once({name, meta, args} = original, env)
when is_atom(name) and is_list(args) and is_list(meta) do
defp do_expand_once({atom, meta, args} = original, env)
when is_atom(atom) and is_list(args) and is_list(meta) do
arity = length(args)
case Macro.Env.expand_import(env, meta, name, arity) do
{:macro, _receiver, expander} ->
# We don't want the line to propagate yet, but generated might!
{expander.(Keyword.take(meta, [:generated]), args), true}
if special_form?(atom, arity) do
{original, false}
else
module = env.module
{:function, Kernel, op} when op in [:+, :-] and arity == 1 ->
case expand_once(hd(args), env) do
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
_ -> {original, false}
extra =
if function_exported?(module, :__info__, 1) do
[{module, module.__info__(:macros)}]
else
[]
end
{:function, _receiver, _name} ->
{original, false}
s = :elixir_env.env_to_ex(env)
{:error, :not_found} ->
{original, false}
expand =
:elixir_dispatch.expand_import(meta, {atom, length(args)}, args, s, env, extra, true)
{:error, other} ->
:elixir_errors.file_error(meta, env, :elixir_dispatch, {:import, other, name, arity})
case expand do
{:ok, receiver, quoted} ->
next = :elixir_module.next_counter(module)
# We don't want the line to propagate yet, but generated might!
meta = Keyword.take(meta, [:generated])
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
{:ok, Kernel, op, [arg]} when op in [:+, :-] ->
case expand_once(arg, env) do
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
_ -> {original, false}
end
{:ok, _receiver, _name, _args} ->
{original, false}
:error ->
{original, false}
end
end
end
# Expand possible macro require invocation
defp do_expand_once({{:., _, [left, name]}, meta, args} = original, env) when is_atom(name) do
defp do_expand_once({{:., _, [left, right]}, meta, args} = original, env) when is_atom(right) do
{receiver, _} = do_expand_once(left, env)
case is_atom(receiver) do
@@ -1886,10 +1903,16 @@ defmodule Macro do
{original, false}
true ->
case Macro.Env.expand_require(env, meta, receiver, name, length(args)) do
{:macro, _receiver, expander} ->
s = :elixir_env.env_to_ex(env)
name_arity = {right, length(args)}
expand = :elixir_dispatch.expand_require(meta, receiver, name_arity, args, s, env)
case expand do
{:ok, receiver, quoted} ->
next = :elixir_module.next_counter(env.module)
# We don't want the line to propagate yet, but generated might!
{expander.(Keyword.take(meta, [:generated]), args), true}
meta = Keyword.take(meta, [:generated])
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
:error ->
{original, false}
@@ -1944,8 +1967,8 @@ defmodule Macro do
def operator?(name, 1) when is_atom(name),
do: Identifier.unary_op(name) != :error
def operator?(:.., 0), do: true
def operator?(:..., 0), do: true
def operator?(:.., 0),
do: true
def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false
+42 -354
View File
@@ -6,15 +6,9 @@ defmodule Macro.Env do
`__ENV__/0`. Inside macros, the caller environment can be
accessed as `__CALLER__/0`.
The majority of the functions in this module are provided
for low-level tools, which need to integrate with the Elixir
compiler, such as language servers and embedded languages.
For regular usage in Elixir code and macros, you must use
the `Macro` module instead. In particular, avoid modifying
the `Macro.Env` struct directly and prefer to use high-level
constructs, such as a `import`, `aliases`, and so forth to
build your own environment. For example, to build a custom
environment, you can define a function such as:
An instance of `Macro.Env` must not be modified by hand. If you need to
create a custom environment to pass to `Code.eval_quoted/3`, use the
following trick:
def make_custom_env do
import SomeModule, only: [some_function: 2], warn: false
@@ -22,9 +16,10 @@ defmodule Macro.Env do
__ENV__
end
## Struct fields
You may then call `make_custom_env()` to get a struct with the desired
imports and aliases included.
The `Macro.Env` struct contains the following fields:
It contains the following fields:
* `context` - the context of the environment; it can be `nil`
(default context), `:guard` (inside a guard) or `:match` (inside a match)
@@ -103,7 +98,7 @@ defmodule Macro.Env do
# Define the __struct__ callbacks by hand for bootstrap reasons.
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(:elixir_quote.escape(struct, :none, false))
def __struct__(), do: unquote(:elixir_quote.escape(struct, false, :none))
def __struct__(unquote(kv)), do: unquote(body)
@doc """
@@ -168,13 +163,43 @@ defmodule Macro.Env do
[file: file, line: line]
end
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@doc """
Fetches the alias for the given atom.
Returns `{:ok, alias}` if the alias exists, `:error`
otherwise.
## Examples
iex> alias Foo.Bar, as: Baz
iex> Baz
Foo.Bar
iex> Macro.Env.fetch_alias(__ENV__, :Baz)
{:ok, Foo.Bar}
iex> Macro.Env.fetch_alias(__ENV__, :Unknown)
:error
"""
@doc since: "1.13.0"
@spec fetch_alias(t, atom) :: {:ok, atom} | :error
def fetch_alias(env, atom)
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@doc """
Fetches the macro alias for the given atom.
Returns `{:ok, macro_alias}` if the alias exists, `:error`
otherwise.
A macro alias is only used inside quoted expansion. See
`fetch_alias/2` for a more general example.
"""
@doc since: "1.13.0"
@spec fetch_macro_alias(t, atom) :: {:ok, atom} | :error
def fetch_macro_alias(env, atom)
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
@@ -187,14 +212,6 @@ defmodule Macro.Env do
`{:function | :macro, module}`. The elements in the list
are in no particular order and the order is not guaranteed.
> #### Use only for introspection {: .warning}
>
> This function does not emit compiler tracing events,
> which may block the compiler from correctly tracking
> dependencies. Use this function for reflection purposes
> but to do not use it to expand imports into qualified
> calls. Instead, use `expand_import/5`.
## Examples
iex> Macro.Env.lookup_import(__ENV__, {:duplicate, 2})
@@ -286,337 +303,8 @@ defmodule Macro.Env do
%{env | tracers: [tracer | tracers]}
end
trace_option = """
`:trace` - when set to `false`, it disables compilation tracers and
lexical tracker. This option must only be used by language servers and
other tools that need to introspect code without affecting how it is compiled.
Disabling tracer inside macros or regular code expansion is extremely
discouraged as it blocks the compiler from accurately tracking dependencies\
"""
@doc """
Defines the given `module` as required in the environment.
It does not check or assert the module is available.
This is used by tools which need to mimic the Elixir compiler.
The appropriate `:require` compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
* #{trace_option}
## Examples
iex> env = __ENV__
iex> Macro.Env.required?(env, Integer)
false
iex> {:ok, env} = Macro.Env.define_require(env, [line: 10], Integer)
iex> Macro.Env.required?(env, Integer)
true
If the `:as` option is given, it will also define an alias:
iex> env = __ENV__
iex> {:ok, env} = Macro.Env.define_require(env, [line: 10], Foo.Bar, as: Baz)
iex> Macro.Env.expand_alias(env, [], [:Baz])
{:alias, Foo.Bar}
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
env = :elixir_aliases.require(meta, module, opts, env, trace)
result = :elixir_aliases.alias(meta, module, false, opts, env, trace)
maybe_define_error(result, :elixir_aliases)
end
@doc """
Defines the given `module` as imported in the environment.
It assumes `module` is available. This is used by tools which
need to mimic the Elixir compiler. The appropriate `:import`
compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
* `:emit_warnings` - emit warnings found when defining imports
* #{trace_option}
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key value pairs.
If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], List)
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, List}]
It accepts the same options as `Kernel.SpecialForm.import/2`:
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], Integer, only: :macros)
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[{:macro, Integer}]
## Info callback override
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], SomeModule, [info_callback: fn :functions -> [{:flatten, 1}]; :macros -> [{:some, 2}]; end])
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, SomeModule}]
iex> Macro.Env.lookup_import(env, {:some, 2})
[{:macro, SomeModule}]
"""
@doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
{warnings, opts} = Keyword.pop(opts, :emit_warnings, true)
{info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1)
result = :elixir_import.import(meta, module, opts, env, warnings, trace, info_callback)
maybe_define_error(result, :elixir_import)
end
@doc """
Defines the given `as` an alias to `module` in the environment.
This is used by tools which need to mimic the Elixir compiler.
The appropriate `:alias` compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
* #{trace_option}
## Examples
iex> env = __ENV__
iex> Macro.Env.expand_alias(env, [], [:Baz])
:error
iex> {:ok, env} = Macro.Env.define_alias(env, [line: 10], Foo.Bar, as: Baz)
iex> Macro.Env.expand_alias(env, [], [:Baz])
{:alias, Foo.Bar}
iex> Macro.Env.expand_alias(env, [], [:Baz, :Bat])
{:alias, Foo.Bar.Bat}
If no `:as` option is given, the alias will be inferred from the module:
iex> env = __ENV__
iex> {:ok, env} = Macro.Env.define_alias(env, [line: 10], Foo.Bar)
iex> Macro.Env.expand_alias(env, [], [:Bar])
{:alias, Foo.Bar}
If it is not possible to infer one, an error is returned:
iex> Macro.Env.define_alias(__ENV__, [line: 10], :an_atom)
{:error,
"alias cannot be inferred automatically for module: :an_atom, " <>
"please use the :as option. Implicit aliasing is only supported with Elixir modules"}
"""
@doc since: "1.17.0"
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_alias(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
result = :elixir_aliases.alias(meta, module, true, opts, env, trace)
maybe_define_error(result, :elixir_aliases)
end
defp maybe_define_error({:ok, env}, _mod),
do: {:ok, env}
defp maybe_define_error({:error, reason}, mod),
do: {:error, Kernel.to_string(mod.format_error(reason))}
@doc """
Expands an alias given by the alias segments.
It returns `{:alias, alias}` if the segments is a list
of atoms and an alias was found. Returns `:error` otherwise.
This expansion may emit the `:alias_expansion` trace event
but it does not emit the `:alias_reference` one.
## Options
* #{trace_option}
## Examples
iex> alias List, as: MyList
iex> Macro.Env.expand_alias(__ENV__, [], [:MyList])
{:alias, List}
iex> Macro.Env.expand_alias(__ENV__, [], [:MyList, :Nested])
{:alias, List.Nested}
If there is no alias or the alias starts with `Elixir.`
(which disables aliasing), then `:error` is returned:
iex> alias List, as: MyList
iex> Macro.Env.expand_alias(__ENV__, [], [:Elixir, MyList])
:error
iex> Macro.Env.expand_alias(__ENV__, [], [:AnotherList])
:error
"""
@doc since: "1.17.0"
@spec expand_alias(t, keyword, [atom()], keyword) ::
{:alias, atom()} | :error
def expand_alias(env, meta, list, opts \\ [])
when is_list(meta) and is_list(list) and is_list(opts) do
trace = Keyword.get(opts, :trace, true)
case :elixir_aliases.expand(meta, list, env, trace) do
atom when is_atom(atom) -> {:alias, atom}
[_ | _] -> :error
end
end
@doc """
Expands an import given by `name` and `arity`.
If the import points to a macro, it returns a tuple
with the module and a function that expands the macro.
The function expects the metadata to be attached to the
expansion and the arguments of the macro.
If the import points to a function, it returns a tuple
with the module and the function name.
If any import is found, the appropriate compiler tracing
event will be emitted.
Otherwise returns `{:error, reason}`.
## Options
* `:allow_locals` - when set to `false`, it does not attempt to capture
local macros defined in the current module in `env`
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
* #{trace_option}
"""
@doc since: "1.17.0"
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| {:function, module(), atom()}
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
def expand_import(env, meta, name, arity, opts \\ [])
when is_list(meta) and is_atom(name) and is_integer(arity) and is_list(opts) do
case :elixir_import.special_form(name, arity) do
true ->
{:error, :not_found}
false ->
allow_locals = Keyword.get(opts, :allow_locals, true)
trace = Keyword.get(opts, :trace, true)
module = env.module
extra =
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
end
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
{:macro, receiver, expander} ->
{:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)}
{:function, receiver, name} ->
{:function, receiver, name}
error ->
{:error, error}
end
end
end
@doc """
Expands a require given by `module`, `name`, and `arity`.
If the require points to a macro and the module has been
required, it returns a tuple with the module and a function
that expands the macro. The function expects the metadata
to be attached to the expansion and the arguments of the macro.
The appropriate `:remote_macro` compiler tracing event will
be emitted if a macro is found (note a `:remote_function`
event is not emitted in `:error` cases).
Otherwise returns `:error`.
## Options
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
* #{trace_option}
"""
@doc since: "1.17.0"
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| :error
def expand_require(env, meta, module, name, arity, opts \\ [])
when is_list(meta) and is_atom(module) and is_atom(name) and is_integer(arity) and
is_list(opts) do
trace = Keyword.get(opts, :trace, true)
case :elixir_dispatch.expand_require(meta, module, name, arity, env, trace) do
{:macro, receiver, expander} ->
{:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)}
:error ->
:error
end
end
defp wrap_expansion(receiver, expander, meta, name, arity, env, opts) do
fn expansion_meta, args ->
if Keyword.get(opts, :check_deprecations, true) do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
end
@doc """
Returns an environment in the guard context.
"""
@doc since: "1.17.0"
@spec to_guard(t) :: t
def to_guard(%{__struct__: Macro.Env} = env) do
%{env | context: :guard}
end
@doc """
Returns an environment in the match context.
Returns a `Macro.Env` in the match context.
"""
@spec to_match(t) :: t
def to_match(%{__struct__: Macro.Env} = env) do
+2 -7
View File
@@ -538,9 +538,7 @@ defmodule Module do
undefined functions, deprecations, etc. A module is always verified after
it is compiled. In Mix projects, a module is also verified when any of its
runtime dependencies change. Therefore this is useful to perform verification
of the current module while avoiding compile-time dependencies. Given the
callback is invoked under different scenarios, Elixir provides no guarantees
of when in the compilation cycle nor in which process the callback runs.
of the current module while avoiding compile-time dependencies.
Accepts a module or a `{module, function_name}` tuple. The function
must take one argument: the module name. When just a module is provided,
@@ -1443,7 +1441,7 @@ defmodule Module do
"to defoverridable/1 because #{error_explanation}"
end
behaviour_callbacks = Module.Behaviour.callbacks(behaviour)
behaviour_callbacks = Module.Types.Behaviour.callbacks(behaviour)
tuples =
for definition <- definitions_in(module),
@@ -1960,9 +1958,6 @@ defmodule Module do
{set, bag} = data_tables_for(module)
case :ets.lookup(set, key) do
[{_, _, :unset, _}] ->
default
[{_, _, :accumulate, traces}] ->
trace_attribute(trace?, module, traces, set, key, [])
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
+20 -36
View File
@@ -250,7 +250,7 @@ defmodule Module.ParallelChecker do
|> merge_compiler_no_warn_undefined()
behaviour_warnings =
Module.Behaviour.check_behaviours_and_impls(
Module.Types.Behaviour.check_behaviours_and_impls(
module,
file,
line,
@@ -259,7 +259,7 @@ defmodule Module.ParallelChecker do
definitions
)
diagnostics =
warnings =
module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
@@ -270,7 +270,7 @@ defmodule Module.ParallelChecker do
|> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
diagnostics
warnings
end
defp extract_no_warn_undefined(compile_opts) do
@@ -291,58 +291,42 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_warnings(warnings) do
{ungrouped, grouped} =
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, warning, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
{ungrouped, grouped}
else
{[{[location], diagnostic} | ungrouped], grouped}
end
end)
grouped =
Enum.map(grouped, fn {_warning, {locations, diagnostic}} ->
{Enum.sort(locations), diagnostic}
end)
Enum.sort(ungrouped ++ grouped)
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
defp emit_warnings(warnings, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1))
log? and print_diagnostics(diagnostics)
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
diagnostics = Enum.map(locations, &to_diagnostic(message, &1))
log? and print_warning(message, diagnostics)
diagnostics
end)
end
defp print_diagnostics([diagnostic]) do
:elixir_errors.print_diagnostic(diagnostic, true)
defp print_warning(message, [diagnostic]) do
:elixir_errors.print_warning(message, diagnostic)
end
defp print_diagnostics(diagnostics) do
:elixir_errors.print_diagnostics(diagnostics)
defp print_warning(message, grouped_warnings) do
:elixir_errors.print_warning_group(message, grouped_warnings)
end
defp to_diagnostic(diagnostic, {file, position, mfa}) when is_list(position) do
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do
%{
severity: :warning,
source: file,
file: file,
position: position_to_tuple(position),
message: IO.iodata_to_binary(message),
stacktrace: [to_stacktrace(file, position, mfa)],
span: nil
}
|> Map.merge(diagnostic)
end
defp position_to_tuple(position) do
+461 -21
View File
@@ -1,24 +1,31 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Expr, Pattern}
defmodule Error do
defexception [:message]
end
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern, Unify}
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
context = context()
stack = stack()
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
try do
warnings_from_clause(meta, args, guards, body, stack, context)
warnings_from_clause(args, guards, body, def_expr, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
error =
RuntimeError.exception("""
Error.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
@@ -27,7 +34,7 @@ defmodule Module.Types do
#{Macro.to_string(def_expr)}
Please report this bug at: https://github.com/elixir-lang/elixir/issues
In case it is a bug, please report it at: https://github.com/elixir-lang/elixir/issues
""")
reraise error, __STACKTRACE__
@@ -51,20 +58,21 @@ defmodule Module.Types do
guards_to_expr(guards, {:when, [], [left, guard]})
end
defp warnings_from_clause(meta, args, guards, body, stack, context) do
with {:ok, _types, context} <- Pattern.of_head(args, guards, meta, stack, context),
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
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, :dynamic, stack, context) do
context.warnings
else
{:error, context} -> context.warnings
{:error, {type, error, context}} ->
[error_to_warning(type, error, context) | context.warnings]
end
end
@doc false
def stack(file, module, function, no_warn_undefined, cache) do
def context(file, module, function, no_warn_undefined, cache) do
%{
# The fallback meta used for literals in patterns and guards
meta: [],
# File of module
file: file,
# Module of definitions
@@ -75,18 +83,450 @@ defmodule Module.Types do
no_warn_undefined: no_warn_undefined,
# A list of cached modules received from the parallel compiler
cache: cache,
# If variable refinements is enabled or not
refine: true
# Expression variable to type variable
vars: %{},
# Type variable to expression variable
types_to_vars: %{},
# Type variable to type
types: %{},
# Trace of all variables that have been refined to a type,
# including the type they were refined to, why, and where
traces: %{},
# Counter to give type variables unique names
counter: 0,
# Track if a variable was inferred from a type guard function such is_tuple/1
# or a guard function that fails such as elem/2, possible values are:
# `:guarded` when `is_tuple(x)`
# `:guarded` when `is_tuple and elem(x, 0)`
# `:fail` when `elem(x, 0)`
guard_sources: %{},
# A list with all warnings from the running the code
warnings: []
}
end
@doc false
def context() do
def stack() do
%{
# A list of all warnings found so far
warnings: [],
# Information about all vars and their types
vars: %{}
# 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
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,
# There are two factors that control how we track guards.
#
# * consider_type_guards?: if type guards should be considered.
# This applies only at the root and root-based "and" and "or" nodes.
#
# * keep_guarded? - if a guarded clause should remain as guarded
# even on failure. Used on the right side of and.
#
type_guards: {_consider_type_guards? = true, _keep_guarded? = false},
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: nil
}
end
## ERROR TO WARNING
# Collect relevant information from context and traces to report error
def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do
{fun, arity} = context.function
location = {context.file, get_position(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[signature | args], traces} = lift_all_types([signature | args], traces, context)
error = {:unable_apply, mfa, args, expected, signature, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
location = {context.file, get_position(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[left, right], traces} = lift_all_types([left, right], traces, context)
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
defp get_position(stack) do
get_meta(stack.last_expr)
end
# Collect relevant traces from context.traces using stack.unify_stack
defp type_traces(stack, context) do
# TODO: Do we need the unify_stack or is enough to only get the last variable
# in the stack since we get related variables anyway?
stack =
stack.unify_stack
|> Enum.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, &tag_trace(expr_var, &1, context))
else
_other -> []
end
end)
end
defp related_variables(var, types) do
Enum.flat_map(types, fn
{related_var, {:var, ^var}} ->
[related_var | related_variables(related_var, types)]
_ ->
[]
end)
end
# Tag if trace is for a concrete type or type variable
defp tag_trace(var, {type, expr, location}, context) do
with {:var, var_index} <- type,
%{^var_index => expr_var} <- context.types_to_vars do
{:var, var, expr_var, expr, location}
else
_ -> {:type, var, type, expr, location}
end
end
defp lift_all_types(types, traces, context) do
trace_types = for({:type, _, type, _, _} <- traces, do: type)
{types, lift_context} = Unify.lift_types(types, context)
{trace_types, _lift_context} = Unify.lift_types(trace_types, lift_context)
{traces, []} =
Enum.map_reduce(traces, trace_types, fn
{:type, var, _, expr, location}, [type | acc] -> {{:type, var, type, expr, location}, acc}
other, acc -> {other, acc}
end)
{types, traces}
end
## FORMAT WARNINGS
def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do
{original_module, original_function, arity} = mfa
{_, _, args} = mfa_or_fa = erl_to_ex(original_module, original_function, args, [])
{module, function, ^arity} = call_to_mfa(mfa_or_fa)
format_mfa = Exception.format_mfa(module, function, arity)
{traces, [] = _hints} = format_traces(traces, [], false)
clauses =
Enum.map(signature, fn {ins, out} ->
{_, _, ins} = erl_to_ex(original_module, original_function, ins, [])
{:fun, [{ins, out}]}
|> Unify.format_type(false)
|> IO.iodata_to_binary()
|> binary_slice(1..-2//1)
end)
[
"expected #{format_mfa} to have signature:\n\n ",
Enum.map_join(args, ", ", &Unify.format_type(&1, false)),
" -> #{Unify.format_type(expected, false)}",
"\n\nbut it has signature:\n\n ",
indent(Enum.join(clauses, "\n")),
"\n\n",
format_expr(expr, location),
traces,
"Conflict found at"
]
end
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
if map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) do
{: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, [left, right], true)
[
"undefined field \"#{atom}\" ",
format_expr(expr, location),
"expected one of the following fields: ",
Enum.join(Enum.sort(known_atoms), ", "),
"\n\n",
traces,
format_message_hints(hints),
"Conflict found at"
]
else
simplify_left? = simplify_type?(left, right)
simplify_right? = simplify_type?(right, left)
{traces, hints} = format_traces(traces, [left, right], 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([], _types, _simplify?) do
{[], []}
end
defp format_traces(traces, types, simplify?) do
traces
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map_reduce([], fn
{:type, var, type, expr, location}, hints ->
{hint, hints} = format_type_hint(type, types, expr, hints)
trace = [
"where \"",
Macro.to_string(var),
"\" was given the type ",
Unify.format_type(type, simplify?),
hint,
" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
{:var, var1, var2, expr, location}, hints ->
trace = [
"where \"",
Macro.to_string(var1),
"\" was given the same type as \"",
Macro.to_string(var2),
"\" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
end)
end
defp format_location({file, position, _mfa}) do
format_location({file, position[:line]})
end
defp format_location({file, line}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line)], else: []
[file, ?:, line, ?\n]
end
defp simplify_type?(type, other) do
map_like_type?(type) and not map_like_type?(other)
end
## EXPRESSION FORMATTING
defp format_expr(nil, _location) do
[]
end
defp format_expr(expr, location) do
[
"in expression:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
end
@doc false
def expr_to_string(expr) do
expr
|> reverse_rewrite()
|> Macro.to_string()
end
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
## Hints
defp format_message_hints(hints) do
hints
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map(&[format_message_hint(&1), "\n"])
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_message_hint({:sized_and_unsize_tuples, {size, var}}) do
"""
#{hint()} use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \
tuple_size(#{Macro.to_string(var)}) == #{size}" to guard a sized tuple.
"""
end
defp hint, do: :elixir_errors.prefix(:hint)
defp format_type_hint(type, types, expr, hints) do
case format_type_hint(type, types, expr) do
{message, hint} -> {message, [hint | hints]}
:error -> {[], hints}
end
end
defp format_type_hint(type, types, 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}
message = sized_and_unsize_tuples(expr, types) ->
{[], {:sized_and_unsize_tuples, message}}
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
defp sized_and_unsize_tuples({{:., _, [:erlang, :is_tuple]}, _, [var]}, types) do
case Enum.find(types, &match?({:tuple, _, _}, &1)) do
{:tuple, size, _} ->
{size, var}
nil ->
nil
end
end
defp sized_and_unsize_tuples(_expr, _types) do
nil
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 map_like_type?({:map, _}), do: true
defp map_like_type?({:union, union}), do: Enum.any?(union, &map_like_type?/1)
defp map_like_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
defp call_to_mfa({{:., _, [mod, fun]}, _, args}), do: {mod, fun, length(args)}
defp call_to_mfa({fun, _, args}) when is_atom(fun), do: {Kernel, fun, length(args)}
end
@@ -1,4 +1,4 @@
defmodule Module.Behaviour do
defmodule Module.Types.Behaviour do
# Checking functionality for @behaviours and @impl
@moduledoc false
@@ -284,11 +284,7 @@ defmodule Module.Behaviour do
end
end
def format_diagnostic(warning) do
%{message: IO.iodata_to_binary(format_warning(warning))}
end
defp format_warning({:undefined_behaviour, module, behaviour}) do
def format_warning({:undefined_behaviour, module, behaviour}) do
[
"@behaviour ",
inspect(behaviour),
@@ -298,12 +294,12 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:module_does_not_define_behaviour, module, behaviour}) do
def format_warning({:module_does_not_define_behaviour, module, behaviour}) do
["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"]
end
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
[
"the behaviour ",
inspect(behaviour),
@@ -315,7 +311,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
[
"conflicting behaviours found. Callback ",
format_definition(kind, callback),
@@ -329,7 +325,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:missing_callback, module, callback, kind, behaviour}) do
def format_warning({:missing_callback, module, callback, kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" is not implemented (in module ",
@@ -338,7 +334,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
def format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" was implemented as \"",
@@ -351,14 +347,14 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:private_function, callback, kind}) do
def format_warning({:private_function, callback, kind}) do
[
format_definition(kind, callback),
" is private, @impl attribute is always discarded for private functions/macros"
]
end
defp format_warning({:no_behaviours, callback, kind, value}) do
def format_warning({:no_behaviours, callback, kind, value}) do
[
"got \"@impl ",
inspect(value),
@@ -368,7 +364,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
def format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
[
"got \"@impl false\" for ",
format_definition(kind, callback),
@@ -377,7 +373,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:impl_defined, callback, kind, callbacks}) do
def format_warning({:impl_defined, callback, kind, callbacks}) do
[
"got \"@impl true\" for ",
format_definition(kind, callback),
@@ -386,7 +382,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
def format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -396,7 +392,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
def format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -407,7 +403,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:missing_impl, callback, kind, behaviour}) do
def format_warning({:missing_impl, callback, kind, behaviour}) do
[
"module attribute @impl was not set for ",
format_definition(kind, callback),
File diff suppressed because it is too large Load Diff
+326 -333
View File
@@ -2,257 +2,312 @@ defmodule Module.Types.Expr do
@moduledoc false
alias Module.Types.{Of, Pattern}
import Module.Types.{Helpers, Descr}
import Module.Types.{Helpers, Unify}
14 = length(Macro.Env.__info__(:struct))
@caller closed_map(
__struct__: atom([Macro.Env]),
aliases: list(),
context: atom([:match, :guard, nil]),
context_modules: list(),
file: binary(),
function: union(tuple(), atom([nil])),
functions: list(),
lexical_tracker: union(pid(), atom([nil])),
line: integer(),
macro_aliases: list(),
macros: list(),
module: atom(),
requires: list(),
tracers: list(),
versioned_vars: open_map()
)
@atom_true atom([true])
@exception open_map(__struct__: atom(), __exception__: @atom_true)
defp of_expr(expr, expected_expr, stack, context) do
with {:ok, actual, context} <- of_expr(expr, stack, context) do
Of.intersect(actual, expected_expr, stack, context)
end
def of_expr(expr, expected, %{context: stack_context} = stack, context)
when stack_context != :expr do
of_expr(expr, expected, %{stack | context: :expr}, context)
end
# :atom
def of_expr(atom, _stack, context) when is_atom(atom) do
{:ok, atom([atom]), context}
def of_expr(atom, _expected, _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}
def of_expr(literal, _expected, _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}
def of_expr(literal, _expected, _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}
def of_expr(literal, _expected, _stack, context) when is_binary(literal) do
{:ok, :binary, context}
end
# #PID<...>
def of_expr(literal, _stack, context) when is_pid(literal) do
{:ok, pid(), context}
end
# []
def of_expr([], _stack, context) do
{:ok, empty_list(), context}
end
# TODO: [expr, ...]
def of_expr(exprs, stack, context) when is_list(exprs) do
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, _types, context} -> {:ok, non_empty_list(), context}
{:error, context} -> {:error, context}
end
end
# {left, right}
def of_expr({left, right}, stack, context) do
of_expr({:{}, [], [left, right]}, stack, context)
def of_expr(literal, _expected, _stack, context) when is_pid(literal) do
{:ok, :dynamic, context}
end
# <<...>>>
def of_expr({:<<>>, _meta, args}, stack, context) do
case Of.binary(args, :expr, stack, context, &of_expr/4) do
{:ok, context} -> {:ok, binary(), context}
# It is safe to discard errors from binary inside expressions
{:error, context} -> {:ok, binary(), context}
def of_expr({:<<>>, _meta, args}, _expected, stack, context) do
case Of.binary(args, stack, context, &of_expr/4) do
{:ok, context} -> {:ok, :binary, context}
{:error, reason} -> {:error, reason}
end
end
# TODO: left | []
def of_expr({:|, _meta, [left_expr, []]}, stack, context) do
of_expr(left_expr, stack, context)
# left | []
def of_expr({:|, _meta, [left_expr, []]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
of_expr(left_expr, :dynamic, stack, context)
end
# TODO: left | right
def of_expr({:|, _meta, [left_expr, right_expr]}, stack, context) do
case of_expr(left_expr, stack, context) do
{:ok, _left, context} ->
of_expr(right_expr, stack, context)
# left | right
def of_expr({:|, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
{:error, context} ->
{:error, context}
case of_expr(left_expr, :dynamic, stack, context) do
{:ok, left, context} ->
case of_expr(right_expr, :dynamic, 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({:__CALLER__, _meta, var_context}, _stack, context)
# []
def of_expr([], _expected, _stack, context) do
{:ok, {:list, :dynamic}, context}
end
# [expr, ...]
def of_expr(exprs, _expected, stack, context) when is_list(exprs) do
stack = push_expr_stack(exprs, stack)
case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, 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}, _expected, _stack, context)
when is_atom(var_context) do
{:ok, @caller, context}
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
# TODO: __STACKTRACE__
def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context)
# __STACKTRACE__
def of_expr({:__STACKTRACE__, _meta, var_context}, _expected, _stack, context)
when is_atom(var_context) do
{:ok, list(), context}
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
# TODO: {...}
def of_expr({:{}, _meta, exprs}, stack, context) do
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
{:ok, _types, context} -> {:ok, tuple(), context}
{:error, context} -> {:error, context}
# var
def of_expr(var, _expected, _stack, context) when is_var(var) do
{:ok, get_var!(var, context), context}
end
# {left, right}
def of_expr({left, right}, expected, stack, context) do
of_expr({:{}, [], [left, right]}, expected, stack, context)
end
# {...}
def of_expr({:{}, _meta, exprs} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
{:error, reason} -> {:error, reason}
end
end
# TODO: left = right
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
with {:ok, right_type, context} <- of_expr(right_expr, stack, context) do
Pattern.of_pattern(left_expr, {right_type, expr}, stack, context)
end
# left = right
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do
# TODO: We might want to bring the expected type forward in case the type of this
# pattern is not useful. For example: 1 = _ = expr
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, left_type, stack, context),
do: unify(right_type, left_type, stack, context)
end
# %{map | ...}
def of_expr({:%{}, _, [{:|, _, [map, args]}]}, stack, context) do
with {:ok, _args_type, context} <- Of.closed_map(args, stack, context, &of_expr/3),
{:ok, _map_type, context} <- of_expr(map, stack, context) do
# TODO: intersect map with keys of terms for args
# TODO: Merge args_type into map_type with dynamic/static key requirement
{:ok, dynamic(open_map()), context}
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
map_type = {:map, [{:optional, :dynamic, :dynamic}]}
with {:ok, map_type, context} <- of_expr(map, map_type, stack, context),
{:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/4),
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(map_type, args_type, stack, context) do
# Retrieve map type and overwrite with the new value types from the map update
case resolve_var(type, context) do
{:map, pairs} ->
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}
_ ->
{:ok, :dynamic, context}
end
end
end
# %Struct{map | ...}
def of_expr(
{:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr,
{:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr,
_expected,
stack,
context
) do
with {:ok, args_types, context} <-
map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) ->
with {:ok, type, context} <- of_expr(value, stack, context) do
{:ok, {key, type}, context}
end
end),
{:ok, struct_type, context} <-
Of.struct(module, args_types, :only_defaults, struct_meta, stack, context),
{:ok, map_type, context} <- of_expr(map, stack, context) do
if empty?(intersection(struct_type, map_type)) do
warning = {:badupdate, :struct, expr, struct_type, map_type, context}
{:ok, dynamic(), warn(__MODULE__, warning, update_meta, stack, context)}
else
# TODO: Merge args_type into map_type with dynamic/static key requirement
Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context)
end
stack = push_expr_stack(expr, stack)
map_type = {:map, [{:optional, :dynamic, :dynamic}]}
with {:ok, struct, context} <- Of.struct(module, meta, context),
{:ok, update, context} <- of_expr(update, map_type, stack, context) do
unify(update, struct, stack, context)
end
end
# %{...}
def of_expr({:%{}, _meta, args}, stack, context) do
Of.closed_map(args, stack, context, &of_expr/3)
def of_expr({:%{}, _meta, args} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
Of.closed_map(args, stack, context, &of_expr/4)
end
# %Struct{}
def of_expr({:%, _, [module, {:%{}, _, args}]} = expr, stack, context) do
Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3)
end
# %Struct{...}
def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
# ()
def of_expr({:__block__, _meta, []}, _stack, context) do
{:ok, atom([nil]), context}
end
# (expr; expr)
def of_expr({:__block__, _meta, exprs}, stack, context) do
{pre, [post]} = Enum.split(exprs, -1)
result =
map_reduce_ok(pre, context, fn expr, context ->
of_expr(expr, stack, context)
end)
case result do
{:ok, _, context} -> of_expr(post, stack, context)
{:error, context} -> {:error, context}
with {:ok, struct, context} <- Of.struct(module, meta1, context),
{:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/4) do
unify(map, struct, stack, context)
end
end
# TODO: cond do pat -> expr end
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
{result, context} =
reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context ->
with {:ok, _, context} <- of_expr(head, stack, context),
{:ok, _, context} <- of_expr(body, stack, context),
do: {:ok, context}
# ()
def of_expr({:__block__, _meta, []}, _expected, _stack, context) do
{:ok, {:atom, nil}, context}
end
# (expr; expr)
def of_expr({:__block__, _meta, exprs}, expected, stack, context) do
expected_types = List.duplicate(:dynamic, length(exprs) - 1) ++ [expected]
result =
map_reduce_ok(Enum.zip(exprs, expected_types), context, fn {expr, expected}, context ->
of_expr(expr, expected, stack, context)
end)
case result do
:ok -> {:ok, dynamic(), context}
{:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context}
{:error, reason} -> {:error, reason}
end
end
# cond do pat -> expr end
def of_expr({:cond, _meta, [[{:do, clauses}]]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
{result, context} =
reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc ->
case of_expr(head, :dynamic, stack, context) do
{:ok, _, context} ->
with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do
{:ok, keep_warnings(acc, context)}
end
error ->
# Skip the clause if it the head has an error
if meta[:generated], do: {:ok, acc}, else: error
end
end)
case result do
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# TODO: case expr do pat -> expr end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context),
# case expr do pat -> expr end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
with {:ok, _expr_type, context} <- of_expr(case_expr, :dynamic, stack, context),
{:ok, context} <- of_clauses(clauses, stack, context),
do: {:ok, dynamic(), context}
do: {:ok, :dynamic, context}
end
# TODO: fn pat -> expr end
def of_expr({:fn, _meta, clauses}, stack, context) do
# fn pat -> expr end
def of_expr({:fn, _meta, clauses} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
case of_clauses(clauses, stack, context) do
{:ok, context} -> {:ok, fun(), context}
{:error, context} -> {:error, context}
{:ok, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
@try_blocks [:do, :after]
@try_clause_blocks [:catch, :else]
@try_clause_blocks [:catch, :else, :after]
# try do expr end
def of_expr({:try, _meta, [blocks]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
# TODO: try do expr end
def of_expr({:try, _meta, [blocks]}, stack, context) do
{result, context} =
reduce_ok(blocks, context, fn
{:rescue, clauses}, context ->
reduce_ok(clauses, context, fn
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, context ->
of_rescue(var, exceptions, body, expr, [], meta, stack, context)
{:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc ->
{_type, context} = new_pattern_var(var, context)
{:->, meta, [[var], body]}, context ->
of_rescue(var, [], body, var, [:anonymous_rescue], meta, stack, context)
with {:ok, context} <- of_expr_context(body, :dynamic, 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, :dynamic, stack, context) do
{:ok, keep_warnings(acc, context)}
end
end)
{block, body}, context when block in @try_blocks ->
of_expr_context(body, stack, context)
{block, body}, context = acc when block in @try_blocks ->
with {:ok, context} <- of_expr_context(body, :dynamic, 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}
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# TODO: receive do pat -> expr end
def of_expr({:receive, _meta, [blocks]}, stack, context) do
# receive do pat -> expr end
def of_expr({:receive, _meta, [blocks]} = expr, _expected, stack, context) do
stack = push_expr_stack(expr, stack)
{result, context} =
reduce_ok(blocks, context, fn
{:do, {:__block__, _, []}}, context ->
@@ -261,171 +316,139 @@ defmodule Module.Types.Expr do
{:do, clauses}, context ->
of_clauses(clauses, stack, context)
{:after, [{:->, _meta, [head, body]}]}, context ->
with {:ok, _type, context} <- of_expr(head, stack, context),
{:ok, _type, context} <- of_expr(body, stack, context),
do: {:ok, context}
{:after, [{:->, _meta, [head, body]}]}, context = acc ->
with {:ok, _type, context} <- of_expr(head, :dynamic, stack, context),
{:ok, _type, context} <- of_expr(body, :dynamic, stack, context),
do: {:ok, keep_warnings(acc, context)}
end)
case result do
:ok -> {:ok, dynamic(), context}
:ok -> {:ok, :dynamic, context}
:error -> {:error, context}
end
end
# TODO: for pat <- expr do expr end
def of_expr({:for, _meta, [_ | _] = args}, stack, context) do
# for pat <- expr do expr end
def of_expr({:for, _meta, [_ | _] = args} = expr, _expected, 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}
{:ok, :dynamic, context}
end
else
with {:ok, _type, context} <- of_expr(block, stack, context) do
{:ok, dynamic(), context}
with {:ok, _type, context} <- of_expr(block, :dynamic, stack, context) do
{:ok, :dynamic, context}
end
end
end
end
# TODO: with pat <- expr do expr end
def of_expr({:with, _meta, [_ | _] = clauses}, stack, context) do
# with pat <- expr do expr end
def of_expr({:with, _meta, [_ | _] = clauses} = expr, _expected, 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, context} -> {:error, context}
{:ok, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
# TODO: fun.(args)
def of_expr({{:., _meta1, [fun]}, _meta2, args}, stack, context) do
with {:ok, fun_type, context} <- of_expr(fun, stack, context),
{:ok, _args_types, context} <-
map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
context =
case fun_fetch(fun_type, length(args)) do
:ok -> context
:error -> Of.incompatible_warn(fun, fun(), fun_type, stack, context)
end
# fun.(args)
def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, _expected, stack, context) do
# TODO: Use expected type to infer intersection return type
stack = push_expr_stack(expr, stack)
{:ok, dynamic(), context}
with {:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context),
{:ok, _arg_types, context} <-
map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do
{:ok, :dynamic, context}
end
end
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = expr, stack, context)
when not is_atom(callee) and is_atom(key_or_fun) do
with {:ok, type, context} <- of_expr(callee, stack, context) do
if Keyword.get(meta, :no_parens, false) do
Of.map_fetch(expr, type, key_or_fun, stack, context)
else
{mods, context} = Of.remote(type, key_or_fun, 0, [:dot], expr, meta, stack, context)
apply_many(mods, key_or_fun, [], expr, stack, context)
end
# expr.key_or_fun
def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, _expected, 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, :dynamic, 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
# TODO: Use expected type to infer intersection return type
with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context),
{:ok, _map_type, context} <- unify(expr_type, :atom, stack, context),
do: {:ok, :dynamic, context}
end
end
# TODO: expr.fun(arg)
def of_expr({{:., _, [remote, name]}, meta, args} = expr, stack, context) do
with {:ok, remote_type, context} <- of_expr(remote, stack, context),
{:ok, args_types, context} <- map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{mods, context} = Of.remote(remote_type, name, length(args), expr, meta, stack, context)
apply_many(mods, name, args_types, expr, stack, context)
# expr.fun(arg)
def of_expr({{:., _meta1, [expr1, fun]}, meta2, args} = expr2, _expected, stack, context) do
# TODO: Use expected type to infer intersection return type
context = Of.remote(expr1, fun, length(args), meta2, context)
stack = push_expr_stack(expr2, stack)
with {:ok, _expr_type, context} <- of_expr(expr1, :dynamic, stack, context),
{:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context),
{:ok, _arg_types, context} <-
map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do
{:ok, :dynamic, context}
end
end
# TODO: &Foo.bar/1
# &Foo.bar/1
def of_expr(
{:&, _, [{:/, _, [{{:., _, [remote, name]}, meta, []}, arity]}]} = expr,
stack,
{:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]},
_expected,
_stack,
context
)
when is_atom(name) and is_integer(arity) do
with {:ok, remote_type, context} <- of_expr(remote, stack, context) do
# TODO: We cannot return the unions of functions. Do we forbid this?
# Do we check it is always the same return type? Do we simply say it is a function?
{_mods, context} = Of.remote(remote_type, name, arity, expr, meta, stack, context)
{:ok, fun(), context}
end
when is_atom(module) and is_atom(fun) do
context = Of.remote(module, fun, arity, meta, context)
{:ok, :dynamic, context}
end
# &foo/1
# TODO: & &1
def of_expr({:&, _meta, _arg}, _stack, context) do
{:ok, fun(), context}
# & &1
def of_expr({:&, _meta, _arg}, _expected, _stack, context) do
# TODO: Function type
{:ok, :dynamic, context}
end
# TODO: call(arg)
def of_expr({fun, _meta, args}, stack, context)
# fun(arg)
def of_expr({fun, _meta, args} = expr, _expected, stack, context)
when is_atom(fun) and is_list(args) do
with {:ok, _arg_types, context} <-
map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
{:ok, dynamic(), context}
# TODO: Use expected type to infer intersection return type
stack = push_expr_stack(expr, stack)
case map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
end
end
# var
def of_expr(var, _stack, context) when is_var(var) do
{:ok, Of.var(var, context), context}
end
## Try
defp of_rescue(var, exceptions, body, expr, hints, meta, stack, context) do
args = [__exception__: @atom_true]
with {:ok, structs, context} <-
map_reduce_ok(exceptions, context, fn exception, context ->
# Exceptions are not validated in the compiler,
# to avoid export dependencies. So we do it here.
if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do
Of.struct(exception, args, :merge_defaults, meta, stack, context)
else
# Whenever there is a failure (such as undefined function),
# we return dynamic to avoid cascading errors. In this case,
# we can return something a bit more precise than dynamic,
# but we still want an open map to avoid cascading.
context = Of.remote(exception, :__struct__, 0, meta, stack, context)
{:ok, dynamic(open_map([__struct__: atom([exception])] ++ args)), context}
end
end) do
context =
case var do
{:_, _, _} ->
context
_ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
formatter = fn expr ->
{"rescue #{expr_to_string(expr)} ->", hints}
end
{:ok, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context)
context
end
of_expr_context(body, stack, context)
end
end
## Comprehensions
defp for_clause({:<-, meta, [left, expr]}, stack, context) do
defp for_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
with {:ok, _pattern_type, context} <-
Pattern.of_head([pattern], guards, meta, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, :dynamic, 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),
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context),
do: {:ok, context}
end
@@ -434,29 +457,26 @@ defmodule Module.Types.Expr do
end
defp for_clause(expr, stack, context) do
of_expr_context(expr, stack, context)
of_expr_context(expr, :dynamic, stack, context)
end
defp for_option({:into, expr}, stack, context) do
of_expr_context(expr, stack, context)
of_expr_context(expr, :dynamic, stack, context)
end
defp for_option({:reduce, expr}, stack, context) do
of_expr_context(expr, stack, context)
of_expr_context(expr, :dynamic, stack, context)
end
defp for_option({:uniq, _}, _stack, context) do
{:ok, context}
end
## With
defp with_clause({:<-, meta, [left, expr]}, stack, context) do
defp with_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
with {:ok, _pattern_type, context} <-
Pattern.of_head([pattern], guards, meta, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context),
do: {:ok, context}
end
@@ -465,46 +485,38 @@ defmodule Module.Types.Expr do
end
defp with_clause(expr, stack, context) do
of_expr_context(expr, stack, context)
of_expr_context(expr, :dynamic, stack, context)
end
defp with_option({:do, body}, stack, context) do
of_expr_context(body, stack, context)
of_expr_context(body, :dynamic, stack, context)
end
defp with_option({:else, clauses}, stack, context) do
of_clauses(clauses, stack, context)
end
## General helpers
defp apply_many([], _function, _args_types, _expr, _stack, context) do
{:ok, dynamic(), context}
end
defp apply_many([mod], function, args_types, expr, stack, context) do
Of.apply(mod, function, args_types, expr, stack, context)
end
defp apply_many(mods, function, args_types, expr, stack, context) do
with {:ok, returns, context} <-
map_reduce_ok(mods, context, fn mod, context ->
Of.apply(mod, function, args_types, expr, stack, context)
end) do
{:ok, Enum.reduce(returns, &union/2), context}
end
end
defp of_clauses(clauses, stack, context) do
reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context ->
reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc ->
{patterns, guards} = extract_head(head)
with {:ok, _, context} <- Pattern.of_head(patterns, guards, meta, stack, context),
{:ok, _, context} <- of_expr(body, stack, context),
do: {:ok, context}
case Pattern.of_head(patterns, guards, stack, context) do
{:ok, _, context} ->
with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do
{:ok, keep_warnings(acc, context)}
end
error ->
# Skip the clause if it the head has an error
if meta[:generated], do: {:ok, acc}, else: error
end
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)}
@@ -524,37 +536,18 @@ defmodule Module.Types.Expr do
[other]
end
defp of_expr_context(expr, stack, context) do
case of_expr(expr, stack, context) do
defp of_expr_context(expr, expected, stack, context) do
case of_expr(expr, expected, stack, context) do
{:ok, _type, context} -> {:ok, context}
{:error, context} -> {:error, context}
{:error, reason} -> {:error, reason}
end
end
## Warning formatting
defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do
{:dynamic, context}
end
def format_diagnostic({:badupdate, type, expr, expected_type, actual_type, context}) do
traces = Of.collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in #{type} update:
#{expr_to_string(expr) |> indent(4)}
expected type:
#{to_quoted_string(expected_type) |> indent(4)}
but got type:
#{to_quoted_string(actual_type) |> indent(4)}
""",
Of.format_traces(traces)
])
}
defp new_pattern_var(var, context) do
new_var(var, context)
end
end
+131 -73
View File
@@ -15,64 +15,9 @@ defmodule Module.Types.Helpers do
end
@doc """
Formatted hints in typing errors.
Returns unique identifier for the current assignment of the variable.
"""
def format_hints(hints) do
hints
|> Enum.uniq()
|> Enum.map(fn
:inferred_bitstring_spec ->
"""
#{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 behavior.
"""
:dot ->
"""
#{hint()} "var.field" (without parentheses) means "var" is a map() while \
"var.fun()" (with parentheses) means "var" is an atom()
"""
:anonymous_rescue ->
"""
#{hint()} when you rescue without specifying exception names, \
the variable is assigned a type of a struct but all of its fields are unknown. \
If you are trying to access an exception's :message key, either specify the \
exception names or use `Exception.message/1`.
"""
end)
end
defp hint, do: :elixir_errors.prefix(:hint)
@doc """
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
"""
def expr_to_string(expr) do
expr
|> reverse_rewrite()
|> Macro.to_string()
end
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
@doc """
Returns the AST metadata.
@@ -80,22 +25,6 @@ defmodule Module.Types.Helpers do
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Indents new lines.
"""
def indent(content, count) do
String.replace(content, "\n", "\n" <> String.duplicate(" ", count))
end
@doc """
Emits a warnings.
"""
def warn(module, warning, meta, stack, context) do
{fun, arity} = stack.function
location = {stack.file, meta, {stack.module, fun, arity}}
%{context | warnings: [{module, warning, location} | context.warnings]}
end
@doc """
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
and stops on `{:error, reason}`.
@@ -116,6 +45,42 @@ defmodule Module.Types.Helpers do
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
@doc """
Like `Enum.unzip/1` but only continues while `fun` returns `{:ok, elem1, elem2}`
and stops on `{:error, reason}`.
"""
def unzip_ok(list) do
do_unzip_ok(list, [], [])
end
defp do_unzip_ok([{:ok, head1, head2} | tail], acc1, acc2) do
do_unzip_ok(tail, [head1 | acc1], [head2 | acc2])
end
defp do_unzip_ok([{:error, reason} | _tail], _acc1, _acc2), do: {:error, reason}
defp do_unzip_ok([], acc1, acc2), do: {:ok, Enum.reverse(acc1), Enum.reverse(acc2)}
@doc """
Like `Enum.map/2` but only continues while `fun` returns `{:ok, elem}`
and stops on `{:error, reason}`.
"""
def map_ok(list, fun) do
do_map_ok(list, [], fun)
end
defp do_map_ok([head | tail], acc, fun) do
case fun.(head) do
{:ok, elem} ->
do_map_ok(tail, [elem | acc], fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_map_ok([], acc, _fun), do: {:ok, Enum.reverse(acc)}
@doc """
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
and stops on `{:error, reason}`.
@@ -135,4 +100,97 @@ defmodule Module.Types.Helpers do
end
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
@doc """
Like `Enum.flat_map/2` but only continues while `fun` returns `{:ok, list}`
and stops on `{:error, reason}`.
"""
def flat_map_ok(list, fun) do
do_flat_map_ok(list, [], fun)
end
defp do_flat_map_ok([head | tail], acc, fun) do
case fun.(head) do
{:ok, elem} ->
do_flat_map_ok(tail, [elem | acc], fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_ok([], acc, _fun), do: {:ok, Enum.reverse(Enum.concat(acc))}
@doc """
Like `Enum.flat_map_reduce/3` but only continues while `fun` returns `{:ok, list, acc}`
and stops on `{:error, reason}`.
"""
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()]}`
if there are no errors.
"""
def oks_or_errors(list) do
case Enum.split_with(list, &match?({:ok, _}, &1)) do
{oks, []} -> {:ok, Enum.map(oks, fn {:ok, ok} -> ok end)}
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
end
end
@doc """
Combines a list of guard expressions `when x when y when z` to an expression
combined with `or`, `x or y or z`.
"""
# 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
@doc """
Like `Enum.zip/1` but will zip multiple lists together instead of only two.
"""
def zip_many(lists) do
zip_many(lists, [], [[]])
end
defp zip_many([], [], [[] | acc]) do
map_reverse(acc, [], &Enum.reverse/1)
end
defp zip_many([], remain, [last | acc]) do
zip_many(Enum.reverse(remain), [], [[] | [last | acc]])
end
defp zip_many([[] | _], remain, [last | acc]) do
zip_many(Enum.reverse(remain), [], [last | acc])
end
defp zip_many([[elem | list1] | list2], remain, [last | acc]) do
zip_many(list2, [list1 | remain], [[elem | last] | acc])
end
defp map_reverse([], acc, _fun), do: acc
defp map_reverse([head | tail], acc, fun), do: map_reverse(tail, [fun.(head) | acc], fun)
end
+228 -628
View File
@@ -3,199 +3,136 @@ defmodule Module.Types.Of do
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
alias Module.ParallelChecker
import Module.Types.{Helpers, Descr}
@prefix quote(do: ...)
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer()
@float float()
@binary binary()
alias Module.ParallelChecker
## Variables
import Module.Types.Helpers
import Module.Types.Unify
# There are important assumptions on how we work with maps.
#
# First, the keys in the map must be ordered by subtyping.
#
# Second, optional keys must be a superset of the required
# keys, i.e. %{required(atom) => integer, optional(:foo) => :bar}
# is forbidden.
#
# Third, in order to preserve co/contra-variance, a supertype
# must satisfy its subtypes. I.e. %{foo: :bar, atom() => :baz}
# is forbidden, it must be %{foo: :bar, atom() => :baz | :bar}.
#
# Once we support user declared maps, we need to validate these
# assumptions.
@doc """
Fetches the type of a defined variable.
Handles open maps (with dynamic => dynamic).
"""
def var({_name, meta, _context}, context) do
version = Keyword.fetch!(meta, :version)
%{vars: %{^version => %{type: type}}} = context
type
end
@doc """
Refines the type of a variable.
"""
def refine_var(var, type, expr, formatter \\ :default, stack, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
case context.vars do
%{^version => %{type: old_type, off_traces: off_traces} = data} ->
new_type = intersection(type, old_type)
data = %{
data
| type: new_type,
off_traces: new_trace(expr, type, formatter, stack, off_traces)
}
context = put_in(context.vars[version], data)
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error, warn({:refine_var, old_type, type, var, context}, meta, stack, context)}
else
{:ok, new_type, context}
end
%{} ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: new_trace(expr, type, formatter, stack, [])
}
context = put_in(context.vars[version], data)
{:ok, type, context}
end
end
defp new_trace(nil, _type, _formatter, _stack, traces),
do: traces
defp new_trace(expr, type, formatter, stack, traces),
do: [{expr, stack.file, type, formatter} | traces]
## Map/structs
@doc """
Handles fetching a map key.
"""
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
{_optional?, value_type} ->
{:ok, value_type, context}
reason ->
{:ok, dynamic(),
warn({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
end
end
@doc """
Builds a closed map.
"""
def closed_map(pairs, extra \\ [], stack, context, of_fun) do
result =
reduce_ok(pairs, {true, extra, [], context}, fn
{key, value}, {closed?, single, multiple, context} ->
with {:ok, keys, context} <- of_finite_key_type(key, stack, context, of_fun),
{:ok, value_type, context} <- of_fun.(value, stack, context) do
case keys do
:none ->
{:ok, {false, single, multiple, context}}
[key] when multiple == [] ->
{:ok, {closed?, [{key, value_type} | single], multiple, context}}
keys ->
{:ok, {closed?, single, [{keys, value_type} | multiple], context}}
end
def open_map(args, stack, context, of_fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
# If we match on a map such as %{"foo" => "bar"}, we cannot
# assert that %{binary() => binary()}, since we are matching
# only a single binary of infinite possible values. Therefore,
# the correct would be to match it to %{binary() => binary() | var}.
#
# We can skip this in two cases:
#
# 1. If the key is a singleton, then we know that it has no
# other value than the current one
#
# 2. If the value is a variable, then there is no benefit in
# creating another variable, so we can skip it
#
# For now, we skip generating the var itself and introduce
# :dynamic instead.
pairs =
for {key, value} <- pairs, not has_unbound_var?(key, context) do
if singleton?(key, context) or match?({:var, _}, value) do
{key, value}
else
{key, to_union([value, :dynamic], context)}
end
end)
with {:ok, {closed?, single, multiple, context}} <- result do
map =
case Enum.reverse(multiple) do
[] ->
pairs = Enum.reverse(single)
if closed?, do: closed_map(pairs), else: open_map(pairs)
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
pairs = Enum.reverse(single, [{key, type} | t])
if closed?, do: closed_map(pairs), else: open_map(pairs)
end
|> Enum.reduce(&union/2)
end
{:ok, map, context}
end
end
defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{:ok, [key], context}
end
defp of_finite_key_type(key, stack, context, of_fun) do
with {:ok, key_type, context} <- of_fun.(key, stack, context) do
case atom_fetch(key_type) do
{:finite, list} -> {:ok, list, context}
_ -> {:ok, :none, context}
end
end
end
defp cartesian_map(lists) do
case lists do
[] ->
[[]]
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
{:ok, {:map, triplets}, context}
end
end
@doc """
Handles structs creation.
Handles closed maps (without dynamic => dynamic).
"""
def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun)
when is_atom(struct) do
# The compiler has already checked the keys are atoms and which ones are required.
with {:ok, args_types, context} <-
map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) ->
with {:ok, type, context} <- of_fun.(value, stack, context) do
{:ok, {key, type}, context}
end
end) do
struct(struct, args_types, default_handling, meta, stack, context)
def closed_map(args, stack, context, of_fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
{:ok, {:map, closed_to_unions(pairs, context)}, context}
end
end
defp map_pairs(pairs, stack, context, of_fun) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- of_fun.(key, :dynamic, stack, context),
{:ok, value_type, context} <- of_fun.(value, :dynamic, 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 """
Struct handling assuming the args have already been converted.
Handles structs.
"""
# TODO: Allow structs fields to be defined. If the fields are defined,
# then the struct is no longer dynamic. And we need to validate args
# against the struct types.
# TODO: Use the struct default values to define the default types.
def struct(struct, args_types, default_handling, meta, stack, context) do
context = remote(struct, :__struct__, 0, meta, stack, context)
def struct(struct, meta, context) do
context = remote(struct, :__struct__, 0, meta, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
term = term()
defaults =
for %{field: field} <- info, field != :__struct__ do
{field, term}
entries =
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
{:required, {:atom, key}, :dynamic}
end
pairs =
case default_handling do
:merge_defaults -> [{:__struct__, atom([struct])} | defaults] ++ args_types
:skip_defaults -> [{:__struct__, atom([struct])} | args_types]
:only_defaults -> [{:__struct__, atom([struct])} | defaults]
end
{:ok, dynamic(closed_map(pairs)), context}
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
end
## Binary
@@ -206,169 +143,154 @@ defmodule Module.Types.Of do
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _kind, _stack, context, _of_fun) do
def binary([], _stack, context, _of_fun) do
{:ok, context}
end
def binary([head], kind, stack, context, of_fun) do
binary_segment(head, kind, [head], stack, context, of_fun)
def binary([head], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
binary_segment(head, head_stack, context, of_fun)
end
def binary([head | tail], kind, stack, context, of_fun) do
case binary_segment(head, kind, [head, @suffix], stack, context, of_fun) do
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
def binary([head | tail], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
case binary_segment(head, head_stack, context, of_fun) do
{:ok, context} -> binary_many(tail, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_many([last], kind, stack, context, of_fun) do
binary_segment(last, kind, [@prefix, last], stack, context, of_fun)
defp binary_many([last], stack, context, of_fun) do
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
binary_segment(last, last_stack, context, of_fun)
end
defp binary_many([head | tail], kind, stack, context, of_fun) do
case binary_segment(head, kind, [@prefix, head, @suffix], stack, context, of_fun) do
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
defp binary_many([head | tail], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
case binary_segment(head, head_stack, context, of_fun) do
{:ok, context} -> binary_many(tail, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
# If the segment is a literal, the compiler has already checked its validity,
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context, _of_fun)
when is_binary(left) or is_number(left) do
{:ok, context}
end
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, of_fun) do
expected_type =
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context, of_fun) do
expected_type = specifier_info(kind, right)
expr = {:<<>>, meta, args}
with {:ok, _type, context} <- of_fun.(left, {expected_type, expr}, stack, context) do
{:ok, context}
end
end
defp specifier_info(kind, {:-, _, [left, _right]}), do: specifier_info(kind, left)
defp specifier_info(:expr, {:float, _, _}), do: @integer_or_float
defp specifier_info(:expr, {:utf8, _, _}), do: @integer_or_binary
defp specifier_info(:expr, {:utf16, _, _}), do: @integer_or_binary
defp specifier_info(:expr, {:utf32, _, _}), do: @integer_or_binary
defp specifier_info(:pattern, {:utf8, _, _}), do: @integer
defp specifier_info(:pattern, {:utf16, _, _}), do: @integer
defp specifier_info(:pattern, {:utf32, _, _}), do: @integer
defp specifier_info(:pattern, {:float, _, _}), do: @float
defp specifier_info(_kind, {:integer, _, _}), do: @integer
defp specifier_info(_kind, {:bits, _, _}), do: @binary
defp specifier_info(_kind, {:bitstring, _, _}), do: @binary
defp specifier_info(_kind, {:bytes, _, _}), do: @binary
defp specifier_info(_kind, {:binary, _, _}), do: @binary
defp specifier_info(_kind, _specifier), do: @integer
## Apply
def apply(:erlang, name, [left, right], expr, stack, context)
when name in [:>=, :"=<", :>, :<, :min, :max] do
result = if name in [:min, :max], do: union(left, right), else: boolean()
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
match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) ->
warning = {:struct_comparison, expr, context}
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
stack.context == :pattern and utf? and is_binary(expr) ->
{:ok, context}
number_type?(left) and number_type?(right) ->
{:ok, result, context}
empty?(intersection(left, right)) ->
warning = {:mismatched_comparison, expr, context}
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
stack.context == :pattern and float? and is_integer(expr) ->
{:ok, context}
true ->
{:ok, result, context}
with {:ok, type, context} <- of_fun.(expr, expected_type, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
def apply(mod, name, args, expr, stack, context) do
case :elixir_rewrite.inline(mod, name, length(args)) do
{mod, name} -> apply(mod, name, args, expr, stack, context)
false -> {:ok, dynamic(), context}
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
def remote(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{_, mods} ->
context =
Enum.reduce(mods, context, fn mod, context ->
remote(mod, fun, arity, meta, stack, context)
end)
{mods, context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], warn(warning, meta, stack, context)}
end
end
@doc """
Checks a module is a valid remote.
Handles remote calls.
"""
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
if Keyword.get(meta, :runtime_module, false) do
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) do
context
else
ParallelChecker.preload_module(stack.cache, module)
check_export(module, fun, arity, meta, stack, context)
ParallelChecker.preload_module(context.cache, module)
check_export(module, fun, arity, meta, context)
end
end
defp check_export(module, fun, arity, meta, stack, context) do
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
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, mode, :def, reason} ->
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
check_deprecated(mode, module, fun, arity, reason, meta, context)
{:ok, mode, :defmacro, reason} ->
context = warn({:unrequired_module, module, fun, arity}, meta, stack, context)
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
context = warn(meta, context, {:unrequired_module, module, fun, arity})
check_deprecated(mode, module, fun, arity, reason, meta, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, stack) do
warn({:undefined_module, module, fun, arity}, meta, stack, context)
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, stack) do
exports = ParallelChecker.all_exports(stack.cache, module)
warn({:undefined_function, module, fun, arity, exports}, meta, stack, context)
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(:elixir, module, fun, arity, reason, meta, stack, context) do
defp check_deprecated(:elixir, module, fun, arity, reason, meta, context) do
if reason do
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
warn(meta, context, {:deprecated, module, fun, arity, reason})
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> :string.titlecase()
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
warn(meta, context, {:deprecated, module, fun, arity, reason})
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
reason = string |> List.to_string() |> :string.titlecase()
reason = "It will be removed in #{removal}. #{reason}"
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
warn(meta, context, {:deprecated, module, fun, arity, reason})
_ ->
context
@@ -385,381 +307,59 @@ defmodule Module.Types.Of do
#
# 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, _stack), do: false
defp warn_undefined?(_module, :module_info, 0, _stack), do: false
defp warn_undefined?(_module, :module_info, 1, _stack), do: false
defp warn_undefined?(:erlang, :orelse, 2, _stack), do: false
defp warn_undefined?(:erlang, :andalso, 2, _stack), do: false
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, stack) do
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
defp warn_undefined?(module, fun, arity, context) do
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
## Warning helpers
@doc """
Intersects two types and emit an incompatible warning if empty.
"""
def intersect(actual, {expected, expr}, stack, context) do
type = intersection(actual, expected)
if empty?(type) do
{:error, incompatible_warn(expr, expected, actual, stack, context)}
else
{:ok, type, context}
end
defp warn(meta, context, warning) do
{fun, arity} = context.function
location = {context.file, meta, {context.module, fun, arity}}
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
end
@doc """
Emits incompatible types warning for the given expression.
This is a generic warning for when the expected/actual types
themselves may come from several different circumstances.
"""
def incompatible_warn(expr, expected_type, actual_type, stack, context) do
meta = get_meta(expr) || stack.meta
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
warning = {:incompatible, expr, expected_type, actual_type, hints, context}
warn(warning, meta, stack, context)
end
defp warn(warning, meta, stack, context) do
warn(__MODULE__, warning, meta, stack, context)
end
## Traces
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
version = meta[:version]
case vars do
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
name: name,
context: context,
traces: collect_var_traces(off_traces)
})}
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
end)
versions
|> Map.values()
|> Enum.sort_by(& &1.name)
end
defp collect_var_traces(traces) do
traces
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
{formatted_expr, formatter_hints} =
case formatter do
:default -> {expr_to_string(expr), []}
formatter -> formatter.(expr)
end
%{
file: file,
meta: meta,
formatted_expr: formatted_expr,
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: to_quoted_string(type)
}
end)
end
def format_traces(traces) do
Enum.map(traces, &format_trace/1)
end
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
traces =
for trace <- traces do
location =
trace.file
|> Path.relative_to_cwd()
|> Exception.format_file_line(trace.meta[:line])
|> String.replace_suffix(":", "")
[
"""
# type: #{indent(trace.formatted_type, 4)}
# from: #{location}
\
""",
indent(trace.formatted_expr, 4),
?\n,
trace.formatted_hints
]
end
type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
end
defp format_var({var, _, context}), do: format_var(var, context)
defp format_var(var, nil), do: "\"#{var}\""
defp format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
defp expr_hints(_), do: []
## Warning formatting
def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
traces = collect_traces(var, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(traces)
])
}
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_diagnostic({:incompatible, expr, expected_type, actual_type, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in expression:
#{expr_to_string(expr) |> indent(4)}
expected type:
#{to_quoted_string(expected_type) |> indent(4)}
but got type:
#{to_quoted_string(actual_type) |> indent(4)}
""",
format_traces(traces),
format_hints(hints)
])
}
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_diagnostic({:badmap, expr, type, key, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a map or struct when accessing .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints([:dot])
])
}
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_diagnostic({:badkey, expr, type, key, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
span: expr |> get_meta() |> :elixir_env.calculate_span(key) |> Keyword.get(:span),
message:
IO.iodata_to_binary([
"""
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces)
])
}
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
end
def format_diagnostic({:mismatched_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison between incompatible types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
While Elixir can compare across all types, you are comparing \
across types which are always distinct, and the result is either \
always true or always false
"""
])
}
end
def format_diagnostic({:struct_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
end
def format_diagnostic({:undefined_module, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
else
Exception.format_mfa(module, fun, arity)
end
%{
message:
IO.iodata_to_binary([
top,
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]),
group: true
}
end
def format_diagnostic({:undefined_function, module, :__struct__, 0, _exports}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
group: true
}
end
def format_diagnostic({:undefined_function, module, fun, arity, exports}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]),
group: true
}
end
def format_diagnostic({:deprecated, module, fun, arity, reason}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]),
group: true
}
end
def format_diagnostic({:unrequired_module, module, fun, arity}) do
%{
message:
IO.iodata_to_binary([
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]),
group: true
}
end
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
defp empty_if(condition, content) do
if condition, do: "", else: content
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
File diff suppressed because it is too large Load Diff
+992
View File
@@ -0,0 +1,992 @@
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
# :binary
# :pid
# :port
# :reference
#
# Those are the composite types:
#
# {:list, type}
# {:tuple, size, [type]} < :tuple
# {:union, [type]}
# {:map, [{:required | :optional, key_type, value_type}]}
# {:fun, [{params, return}]}
#
# Once new types are added, they should be considered in:
#
# * unify (all)
# * format_type (all)
# * subtype? (subtypes only)
# * recursive_type? (composite only)
# * collect_var_indexes (composite only)
# * lift_types (composite only)
# * flatten_union (composite only)
# * walk (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(same, same, _stack, context) do
{:ok, same, context}
end
def unify({:var, var}, type, stack, context) do
unify_var(var, type, stack, context, _var_source = true)
end
def unify(type, {:var, var}, stack, context) do
unify_var(var, type, stack, context, _var_source = false)
end
def 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
def 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
def unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
unify_maps(source_pairs, target_pairs, stack, context)
end
def unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
def unify(:dynamic, target, _stack, context) do
{:ok, target, context}
end
def unify({:union, types}, target, stack, context) do
unify_result =
map_reduce_ok(types, context, fn type, context ->
unify(type, target, stack, context)
end)
case unify_result do
{:ok, types, context} -> {:ok, to_union(types, context), context}
{:error, context} -> {:error, context}
end
end
def unify(source, target, stack, context) do
cond do
# TODO: This condition exists to handle unions with unbound vars.
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
def unify_var(var, :dynamic, _stack, context, _var_source?) do
{:ok, {:var, var}, context}
end
def 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, _} = var_type} ->
# Do not recursively traverse type vars for now
# to avoid pathological cases related to performance.
{:ok, var_type, context}
%{^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, ^var}, context} ->
{:ok, {:var, var}, context}
{:ok, res_type, context} ->
context = refine_var!(var, res_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.
def 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
def 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
def 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
def 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
def 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
def 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 """
Maybe resolves a variable.
"""
def resolve_var({:var, var}, context) do
case context.types do
%{^var => :unbound} -> {:var, 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 """
Restores the variable information from the old context into new context.
"""
def restore_var!(var, new_context, old_context) do
%{^var => type} = old_context.types
%{^var => trace} = old_context.traces
types = Map.put(new_context.types, var, type)
traces = Map.put(new_context.traces, var, trace)
%{new_context | types: types, traces: traces}
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?({:fun, clauses}, parents, context) do
Enum.any?(clauses, fn {args, return} ->
Enum.any?([return | args], &recursive_type?(&1, [clauses | parents], context))
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Collects all type vars recursively.
"""
def collect_var_indexes(type, context, acc \\ %{}) do
{_type, indexes} =
walk(type, acc, fn
{:var, var}, acc ->
case acc do
%{^var => _} ->
{{:var, var}, acc}
%{} ->
case context.types do
%{^var => :unbound} ->
{{:var, var}, Map.put(acc, var, true)}
%{^var => type} ->
{{:var, var}, collect_var_indexes(type, context, Map.put(acc, var, true))}
end
end
other, acc ->
{other, acc}
end)
indexes
end
@doc """
Checks if the type has a type var.
"""
def has_unbound_var?(type, context) do
walk(type, :ok, fn
{:var, var}, acc ->
case context.types do
%{^var => :unbound} ->
throw(:has_unbound_var?)
%{^var => type} ->
has_unbound_var?(type, context)
{{:var, var}, acc}
end
other, acc ->
{other, acc}
end)
false
catch
:throw, :has_unbound_var? -> true
end
@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.all?(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
case unique_super_types(unnest_unions(types), context) do
[type] -> type
types -> {:union, types}
end
end
defp unnest_unions(types) do
Enum.flat_map(types, fn
{:union, types} -> unnest_unions(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
## Type lifting
@doc """
Lifts type variables to their inferred types from the context.
"""
def lift_types(types, %{lifted_types: _} = context) do
Enum.map_reduce(types, context, &lift_type/2)
end
def lift_types(types, context) do
context = %{
types: context.types,
lifted_types: %{},
lifted_counter: 0
}
Enum.map_reduce(types, context, &lift_type/2)
end
# Lift type variable to its inferred (hopefully concrete) types from the context
defp lift_type({:var, var}, context) do
case context.lifted_types do
%{^var => lifted_var} ->
{{:var, lifted_var}, context}
%{} ->
case context.types do
%{^var => :unbound} ->
new_lifted_var(var, context)
%{^var => type} ->
if recursive_type?(type, [], context) do
new_lifted_var(var, context)
else
# Remove visited types to avoid infinite loops
# then restore after we are done recursing on vars
types = context.types
context = put_in(context.types[var], :unbound)
{type, context} = lift_type(type, context)
{type, %{context | types: types}}
end
%{} ->
new_lifted_var(var, context)
end
end
end
defp lift_type({:union, types}, context) do
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
{{:union, types}, context}
end
defp lift_type({:tuple, n, types}, context) do
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
{{:tuple, n, types}, context}
end
defp lift_type({:map, pairs}, context) do
{pairs, context} =
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
{key, context} = lift_type(key, context)
{value, context} = lift_type(value, context)
{{kind, key, value}, context}
end)
{{:map, pairs}, context}
end
defp lift_type({:list, type}, context) do
{type, context} = lift_type(type, context)
{{:list, type}, context}
end
defp lift_type({:fun, clauses}, context) do
clauses =
Enum.map_reduce(clauses, context, fn {args, return}, context ->
{[return | args], context} = Enum.map_reduce([return | args], context, &lift_type/2)
{{args, return}, context}
end)
{{:fun, clauses}, context}
end
defp lift_type(other, context) do
{other, context}
end
defp new_lifted_var(original_var, context) do
types = Map.put(context.lifted_types, original_var, context.lifted_counter)
counter = context.lifted_counter + 1
type = {:var, context.lifted_counter}
context = %{context | lifted_types: types, lifted_counter: counter}
{type, context}
end
# TODO: Figure out function expansion
@doc """
Expand unions so that all unions are at the top level.
{integer() | float()} => {integer()} | {float()}
"""
def flatten_union({:union, types}, context) do
Enum.flat_map(types, &flatten_union(&1, context))
end
def flatten_union(type, context) do
List.wrap(do_flatten_union(type, context))
end
def do_flatten_union({:tuple, num, types}, context) do
flatten_union_tuple(types, num, context, [])
end
def do_flatten_union({:list, type}, context) do
case do_flatten_union(type, context) do
{:union, union_types} -> Enum.map(union_types, &{:list, &1})
_type -> [{:list, type}]
end
end
def do_flatten_union({:map, pairs}, context) do
flatten_union_map(pairs, context, [])
end
def do_flatten_union({:var, var}, context) do
if looping_var?(var, context, []) do
{:var, var}
else
case context.types do
%{^var => :unbound} -> {:var, var}
%{^var => {:union, types}} -> Enum.map(types, &do_flatten_union(&1, context))
%{^var => type} -> do_flatten_union(type, context)
end
end
end
def do_flatten_union(type, _context) do
type
end
defp flatten_union_tuple([type | types], num, context, acc) do
case do_flatten_union(type, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_tuple(types, num, context, [&1 | acc]))
type ->
flatten_union_tuple(types, num, context, [type | acc])
end
end
defp flatten_union_tuple([], num, _context, acc) do
[{:tuple, num, Enum.reverse(acc)}]
end
defp flatten_union_map([{kind, key, value} | pairs], context, acc) do
case do_flatten_union(key, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_map_value(kind, &1, value, pairs, context, acc))
type ->
flatten_union_map_value(kind, type, value, pairs, context, acc)
end
end
defp flatten_union_map([], _context, acc) do
[{:map, Enum.reverse(acc)}]
end
defp flatten_union_map_value(kind, key, value, pairs, context, acc) do
case do_flatten_union(value, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_map(pairs, context, [{kind, key, &1} | acc]))
value ->
flatten_union_map(pairs, context, [{kind, key, value} | acc])
end
end
defp looping_var?(var, context, parents) do
case context.types do
%{^var => :unbound} ->
false
%{^var => {:var, type}} ->
if var in parents do
true
else
looping_var?(type, context, [var | parents])
end
%{^var => _type} ->
false
end
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
types
|> Enum.map(&format_type(&1, simplify?))
|> Enum.intersperse(" | ")
end
def format_type({:tuple, _, types}, simplify?) do
format =
types
|> Enum.map(&format_type(&1, simplify?))
|> Enum.intersperse(", ")
["{", format, "}"]
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", Integer.to_string(index + 1)]
end
def format_type({:fun, clauses}, simplify?) do
format =
Enum.map(clauses, fn {params, return} ->
params = Enum.intersperse(Enum.map(params, &format_type(&1, simplify?)), ", ")
params = if params == [], do: params, else: [params, " "]
return = format_type(return, simplify?)
[params, "-> ", return]
end)
["(", Enum.intersperse(format, "; "), ")"]
end
def format_type(atom, _simplify?) when is_atom(atom) do
[Atom.to_string(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))
(atoms ++ required ++ optional)
|> Enum.map(fn
{:required, {:atom, atom}, right} ->
[Atom.to_string(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)
|> Enum.intersperse(", ")
end
@doc """
Performs a depth-first, pre-order traversal of the type tree using an accumulator.
"""
def walk({:map, pairs}, acc, fun) do
{pairs, acc} =
Enum.map_reduce(pairs, acc, fn {kind, key, value}, acc ->
{key, acc} = walk(key, acc, fun)
{value, acc} = walk(value, acc, fun)
{{kind, key, value}, acc}
end)
fun.({:map, pairs}, acc)
end
def walk({:union, types}, acc, fun) do
{types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun))
fun.({:union, types}, acc)
end
def walk({:tuple, num, types}, acc, fun) do
{types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun))
fun.({:tuple, num, types}, acc)
end
def walk({:list, type}, acc, fun) do
{type, acc} = walk(type, acc, fun)
fun.({:list, type}, acc)
end
def walk({:fun, clauses}, acc, fun) do
{clauses, acc} =
Enum.map_reduce(clauses, acc, fn {params, return}, acc ->
{params, acc} = Enum.map_reduce(params, acc, &walk(&1, &2, fun))
{return, acc} = walk(return, acc, fun)
{{params, return}, acc}
end)
fun.({:fun, clauses}, acc)
end
def walk(type, acc, fun) do
fun.(type, acc)
end
end
+1 -1
View File
@@ -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_opt/5`.
For the list of available options, see `:erlang.spawn/4`.
Inlined by the compiler.
"""
+24 -27
View File
@@ -152,47 +152,44 @@ defmodule OptionParser do
### Parsing unknown switches
When the `:switches` option is given, `OptionParser` will attempt to parse
unknown switches.
Switches without an argument will be set to `true`:
unknown switches:
iex> OptionParser.parse(["--debug"], switches: [key: :string])
{[debug: true], [], []}
Even though we haven't specified `--debug` in the list of switches, it is part
of the returned options. The same happens for switches followed by another switch:
iex> OptionParser.parse(["--debug", "--ok"], switches: [])
{[debug: true, ok: true], [], []}
Switches followed by a value will be assigned the value, as a string:
of the returned options. This would also work:
iex> OptionParser.parse(["--debug", "value"], switches: [key: :string])
{[debug: "value"], [], []}
Since we cannot assert the type of the switch value, it is preferred to use the
`:strict` option that accepts only known switches and always verify their types.
Switches followed by a value will be assigned the value, as a string. Switches
without an argument will be set automatically to `true`. Since we cannot assert
the type of the switch value, it is preferred to use the `:strict` option that
accepts only known switches and always verify their types.
If you do want to parse unknown switches, remember that Elixir converts switches
to atoms. Since atoms are not garbage-collected, to avoid creating new ones,
OptionParser by default only parses switches that translate to existing atoms.
The code below discards the `--option-parser-example` switch because the
`:option_parser_example` atom is never used anywhere:
to atoms. Since atoms are not garbage-collected, OptionParser will only parse
switches that translate to atoms used by the runtime to avoid leaking atoms.
For instance, the code below will discard the `--option-parser-example` switch
because the `:option_parser_example` atom is never used anywhere:
iex> OptionParser.parse(["--option-parser-example"], switches: [])
{[], [], []}
OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
# The :option_parser_example atom is not used anywhere below
If a switch corresponds to an existing Elixir atom, whether from your
code, a dependency or from Elixir itself, it will be accepted. However,
it is best to not rely on external code, and always define the atoms
you want to parse in the same module that calls `OptionParser` itself,
as direct arguments to the `:switches` or `:strict` options.
However, the code below would work as long as `:option_parser_example` atom is
used at some point later (or earlier) **in the same module**. For example:
If you would like to parse all switches, regardless if they exist or not,
you can force creation of atoms by passing `allow_nonexistent_atoms: true`
as option. Use this option with care. It is only useful when you are building
command-line applications that receive dynamically-named arguments and must
be avoided in long-running systems.
{opts, _, _} = OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
# ... then somewhere in the same module you access it ...
opts[:option_parser_example]
In other words, Elixir will only parse options that are used by the runtime,
ignoring all others. If you would like to parse all switches, regardless if
they exist or not, you can force creation of atoms by passing
`allow_nonexistent_atoms: true` as option. Use this option with care. It is
only useful when you are building command-line applications that receive
dynamically-named arguments and must be avoided in long-running systems.
## Aliases
+1 -10
View File
@@ -64,9 +64,6 @@ defmodule Path do
iex> Path.absname("../x", "bar")
"bar/../x"
iex> Path.absname("foo", fn -> "lazy" end)
"lazy/foo"
"""
@spec absname(t, t | (-> t)) :: binary
def absname(path, relative_to) do
@@ -172,17 +169,11 @@ defmodule Path do
Converts the path to an absolute one, expanding
any `.` and `..` components and a leading `~`.
If a relative path is provided it is expanded relatively to
the current working directory.
## Examples
Path.expand("/foo/bar/../baz")
#=> "/foo/baz"
Path.expand("foo/bar/../baz")
#=> "$PWD/foo/baz"
"""
@spec expand(t) :: binary
def expand(path) do
@@ -267,7 +258,7 @@ defmodule Path do
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
"""
# Note this function does not expand paths because the behavior
# Note this function does not expand paths because the behaviour
# is ambiguous. If we expand it before converting to relative, then
# "/usr/../../foo" means "/foo". If we expand it after, it means "../foo".
# We could expand only relative paths but it is best to say it never
+1 -1
View File
@@ -16,7 +16,7 @@ defmodule Port do
iex> send(port, {self(), :close})
:ok
iex> flush()
{#Port<0.1444>, :closed}
{#Port<0.1464>, :closed}
:ok
In the example above, we have created a new port that executes the
+4 -33
View File
@@ -195,7 +195,7 @@ defmodule Process do
@doc """
Sends an exit signal with the given `reason` to `pid`.
The following behavior applies if `reason` is any term except `:normal`
The following behaviour applies if `reason` is any term except `:normal`
or `:kill`:
1. If `pid` is not trapping exits, `pid` will exit with the given
@@ -624,7 +624,7 @@ defmodule Process do
exits with a reason other than `:normal` (which is also the exit reason used
when a process finishes its job) and `pid1` is not trapping exits (see
`flag/2`), then `pid1` will exit with the same reason as `pid2` and in turn
emit an exit signal to all its other linked processes. The behavior when
emit an exit signal to all its other linked processes. The behaviour when
`pid1` is trapping exits is described in `exit/2`.
See `:erlang.link/1` for more information.
@@ -839,17 +839,13 @@ defmodule Process do
nilify(:erlang.process_info(pid))
end
@type process_info_item :: atom | {:dictionary, term}
@type process_info_result_item :: {process_info_item, term}
@doc """
Returns information about the process identified by `pid`,
or returns `nil` if the process is not alive.
See `:erlang.process_info/2` for more information.
"""
@spec info(pid, process_info_item) :: process_info_result_item | nil
@spec info(pid, [process_info_item]) :: [process_info_result_item] | nil
@spec info(pid, atom | [atom]) :: {atom, term} | [{atom, term}] | nil
def info(pid, spec)
def info(pid, :registered_name) do
@@ -860,7 +856,7 @@ defmodule Process do
end
end
def info(pid, spec) do
def info(pid, spec) when is_atom(spec) or is_list(spec) do
nilify(:erlang.process_info(pid, spec))
end
@@ -944,31 +940,6 @@ defmodule Process do
@spec unalias(alias) :: boolean
defdelegate unalias(alias), to: :erlang
@doc """
Add a descriptive term to the current process.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
This label may be useful for identifying a process as one of multiple in a
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
## Examples
Process.set_label(:worker)
#=> :ok
Process.set_label({:any, "term"})
#=> :ok
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil
defp nilify(other), do: other
+2 -3
View File
@@ -126,8 +126,8 @@ defmodule Protocol do
Although the implementation above is arguably not a reasonable
one. For example, it makes no sense to say a PID or an integer
have a size of `0`. That's one of the reasons why `@fallback_to_any`
is an opt-in behavior. For the majority of protocols, raising
an error when a protocol is not implemented is the proper behavior.
is an opt-in behaviour. For the majority of protocols, raising
an error when a protocol is not implemented is the proper behaviour.
## Multiple implementations
@@ -972,7 +972,6 @@ defmodule Protocol do
Protocol.__ensure_defimpl__(protocol, for, __ENV__)
defmodule name do
@moduledoc false
@behaviour protocol
@protocol protocol
@for for
+7 -7
View File
@@ -172,7 +172,7 @@ defmodule Range do
one element, which is the number itself.
If `first` is greater than `last`, the range will be decreasing from `first`
to `last`, albeit this behavior is deprecated. Therefore, it is advised to
to `last`, albeit this behaviour is deprecated. Therefore, it is advised to
explicitly list the step with `new/3`.
## Examples
@@ -184,7 +184,7 @@ defmodule Range do
@spec new(limit, limit) :: t
def new(first, last) when is_integer(first) and is_integer(last) do
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.18
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.17
step = if first <= last, do: 1, else: -1
%Range{first: first, last: last, step: step}
end
@@ -231,6 +231,8 @@ defmodule Range do
iex> Range.size(1..10//-1)
0
iex> Range.size(10..1)
10
iex> Range.size(10..1//-1)
10
iex> Range.size(10..1//-2)
@@ -426,7 +428,7 @@ defmodule Range do
iex> Range.disjoint?(1..5, 6..9)
true
iex> Range.disjoint?(5..1//-1, 6..9)
iex> Range.disjoint?(5..1, 6..9)
true
iex> Range.disjoint?(1..5, 5..9)
false
@@ -506,10 +508,8 @@ defmodule Range do
@doc false
@deprecated "Pattern match on first..last//step instead"
def range?(%{__struct__: Range, first: first, last: last})
when is_integer(first) and is_integer(last),
do: true
def range?(term)
def range?(first..last) when is_integer(first) and is_integer(last), do: true
def range?(_), do: false
end
+40 -54
View File
@@ -84,7 +84,7 @@ defmodule Regex do
each line; use `\A` and `\z` to match the end or beginning of the string
* `:extended` (x) - whitespace characters are ignored except when escaped
or within `[..]`, and allow `#` to delimit comments
and allow `#` to delimit comments
* `:firstline` (f) - forces the unanchored pattern to match before or at the
first newline, though the matched text may continue over the newline
@@ -122,7 +122,7 @@ defmodule Regex do
* `:all_names` - captures all named subpattern matches in the Regex as a list
ordered **alphabetically** by the names of the subpatterns
* `list(binary | atom)` - a list of named captures to capture
* `list(binary)` - a list of named captures to capture
## Character classes
@@ -152,10 +152,10 @@ defmodule Regex do
There is another character class, `ascii`, that erroneously matches
Latin-1 characters instead of the 0-127 range specified by POSIX. This
cannot be fixed without altering the behavior of other classes, so we
cannot be fixed without altering the behaviour of other classes, so we
recommend matching the range with `[\\0-\x7f]` instead.
Note the behavior of those classes may change according to the Unicode
Note the behaviour of those classes may change according to the Unicode
and other modifiers:
iex> String.match?("josé", ~r/^[[:lower:]]+$/)
@@ -184,7 +184,7 @@ defmodule Regex do
check and recompile the regex if necessary.
"""
defstruct re_pattern: nil, source: "", opts: [], re_version: ""
defstruct re_pattern: nil, source: "", opts: "", re_version: ""
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [term]}
@@ -213,7 +213,7 @@ defmodule Regex do
{:ok, ~r/foo/}
iex> Regex.compile("*foo")
{:error, {~c"nothing to repeat", 0}}
{:error, {'nothing to repeat', 0}}
iex> Regex.compile("foo", "i")
{:ok, ~r/foo/i}
@@ -223,30 +223,39 @@ defmodule Regex do
"""
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
def compile(source, opts \\ "") when is_binary(source) do
compile(source, opts, version())
def compile(source, options \\ "") when is_binary(source) do
compile(source, options, version())
end
defp compile(source, opts, version) when is_binary(opts) do
case translate_options(opts, []) do
defp compile(source, options, version) when is_binary(options) do
case translate_options(options, []) do
{:error, rest} ->
{:error, {:invalid_option, rest}}
translated_opts ->
compile(source, translated_opts, version)
translated_options ->
compile(source, translated_options, options, version)
end
end
defp compile(source, opts, version) when is_list(opts) do
defp compile(source, options, version) when is_list(options) do
compile(source, options, "", version)
end
defp compile(source, opts, doc_opts, version) do
case :re.compile(source, opts) do
{:ok, re_pattern} ->
{:ok, %Regex{re_pattern: re_pattern, re_version: version, source: source, opts: opts}}
doc_opts = format_doc_opts(doc_opts, opts)
{:ok, %Regex{re_pattern: re_pattern, re_version: version, source: source, opts: doc_opts}}
error ->
error
end
end
defp format_doc_opts(_doc_opts = "", _opts = []), do: ""
defp format_doc_opts(_doc_opts = "", opts), do: opts
defp format_doc_opts(doc_opts, _opts), do: doc_opts
@doc """
Compiles the regular expression and raises `Regex.CompileError` in case of errors.
"""
@@ -334,7 +343,7 @@ defmodule Regex do
* `:return` - when set to `:index`, returns byte index and match length.
Defaults to `:binary`.
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
to see the possible capture values.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to zero.
@@ -350,12 +359,6 @@ defmodule Regex do
iex> Regex.run(~r/c(d)/, "abcd", return: :index)
[{2, 2}, {3, 1}]
iex> Regex.run(~r/c(d)/, "abcd", capture: :first)
["cd"]
iex> Regex.run(~r/c(?<foo>d)/, "abcd", capture: ["foo", "bar"])
["d", ""]
"""
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
def run(regex, string, options \\ [])
@@ -423,20 +426,21 @@ defmodule Regex do
end
@doc """
Returns the regex options.
Returns the regex options, as a string or list depending on how
it was compiled.
See the documentation of `Regex.compile/2` for more information.
## Examples
iex> Regex.opts(~r/foo/m)
[:multiline]
"m"
iex> Regex.opts(Regex.compile!("foo", [:caseless]))
[:caseless]
"""
@spec opts(t) :: [term]
@spec opts(t) :: String.t() | [term]
def opts(%Regex{opts: opts}) do
opts
end
@@ -467,7 +471,8 @@ defmodule Regex do
end
@doc ~S"""
Same as `run/3` but returns all non-overlapping matches of the regular expression.
Same as `run/3`, but scans the target several times collecting all
matches of the regular expression.
A list of lists is returned, where each entry in the primary list represents a
match and each entry in the secondary list represents the captured contents.
@@ -476,7 +481,7 @@ defmodule Regex do
* `:return` - when set to `:index`, returns byte index and match length.
Defaults to `:binary`.
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
to see the possible capture values.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to zero.
@@ -492,21 +497,12 @@ defmodule Regex do
iex> Regex.scan(~r/e/, "abcd")
[]
iex> Regex.scan(~r/ab|bc|cd/, "abcd")
[["ab"], ["cd"]]
iex> Regex.scan(~r/ab|bc|cd/, "abbccd")
[["ab"], ["bc"], ["cd"]]
iex> Regex.scan(~r/\p{Sc}/u, "$, £, and €")
[["$"], ["£"], ["€"]]
iex> Regex.scan(~r/=+/, "=ü†ƒ8===", return: :index)
[[{0, 1}], [{9, 3}]]
iex> Regex.scan(~r/c(d|e)/, "abcd abce", capture: :first)
[["cd"], ["ce"]]
"""
@spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
def scan(regex, string, options \\ [])
@@ -530,15 +526,8 @@ defmodule Regex do
options
) do
case version() do
^version ->
:re.run(string, compiled, options)
_ when is_list(compile_opts) ->
:re.run(string, source, compile_opts ++ options)
# TODO: This clause is kept for compatibility with previous Elixir versions. Remove on v2.0+.
_ when is_binary(compile_opts) ->
:re.run(string, source, translate_options(compile_opts, options))
^version -> :re.run(string, compiled, options)
_ -> :re.run(string, source, translate_options(compile_opts, options))
end
end
@@ -558,7 +547,7 @@ defmodule Regex do
* `:on` - specifies which captures to split the string on, and in what
order. Defaults to `:first` which means captures inside the regex do not
affect the splitting process. See the ["Captures" section](#module-captures)
affect the splitting process. Check the moduledoc for `Regex`
to see the possible capture values.
* `:include_captures` - when `true`, includes in the result the matches of
@@ -591,9 +580,6 @@ defmodule Regex do
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second], include_captures: true)
["a", "b", "c"]
iex> Regex.split(~r{-}, "-a-b--c", trim: true)
["a", "b", "c"]
"""
@spec split(t, String.t(), [term]) :: [String.t()]
def split(regex, string, options \\ [])
@@ -723,8 +709,7 @@ defmodule Regex do
"Abcadc"
"""
@spec replace(t, String.t(), String.t() | (... -> String.t()), global: boolean()) ::
String.t()
@spec replace(t, String.t(), String.t() | (... -> String.t()), [term]) :: String.t()
def replace(%Regex{} = regex, string, replacement, options \\ [])
when is_binary(string) and is_list(options) do
opts = if Keyword.get(options, :global) != false, do: [:global], else: []
@@ -898,14 +883,15 @@ defmodule Regex do
# Helpers
defp translate_options(<<?s, t::binary>>, acc),
do: translate_options(t, [:dotall, {:newline, :anycrlf} | acc])
defp translate_options(<<?u, t::binary>>, acc), do: translate_options(t, [:unicode, :ucp | acc])
defp translate_options(<<?i, t::binary>>, acc), do: translate_options(t, [:caseless | acc])
defp translate_options(<<?x, t::binary>>, acc), do: translate_options(t, [:extended | acc])
defp translate_options(<<?f, t::binary>>, acc), do: translate_options(t, [:firstline | acc])
defp translate_options(<<?U, t::binary>>, acc), do: translate_options(t, [:ungreedy | acc])
defp translate_options(<<?s, t::binary>>, acc),
do: translate_options(t, [:dotall, {:newline, :anycrlf} | acc])
defp translate_options(<<?m, t::binary>>, acc), do: translate_options(t, [:multiline | acc])
defp translate_options(<<?r, t::binary>>, acc) do
@@ -914,5 +900,5 @@ defmodule Regex do
end
defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}
defp translate_options(rest, _acc), do: {:error, rest}
end
+13 -13
View File
@@ -18,7 +18,7 @@ defmodule Registry do
implementation. We explore some of those use cases below.
The registry may also be transparently partitioned, which provides
more scalable behavior for running registries on highly concurrent
more scalable behaviour for running registries on highly concurrent
environments with thousands or millions of entries.
## Using in `:via`
@@ -757,28 +757,28 @@ defmodule Registry do
In the example below we register the current process and look it up
both from itself and other processes:
iex> Registry.start_link(keys: :unique, name: Registry.UniqueValuesTest)
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
iex> Registry.start_link(keys: :unique, name: Registry.UniqueLookupTest)
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
[]
iex> {:ok, _} = Registry.register(Registry.UniqueValuesTest, "hello", :world)
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world)
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
[:world]
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", self()) end) |> Task.await()
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", self()) end) |> Task.await()
[]
iex> parent = self()
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", parent) end) |> Task.await()
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", parent) end) |> Task.await()
[:world]
The same applies to duplicate registries:
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateValuesTest)
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateLookupTest)
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :world)
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world)
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
[:world]
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :another)
iex> Enum.sort(Registry.values(Registry.DuplicateValuesTest, "hello", self()))
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another)
iex> Enum.sort(Registry.values(Registry.DuplicateLookupTest, "hello", self()))
[:another, :world]
"""
+8 -71
View File
@@ -1137,53 +1137,11 @@ defmodule Stream do
lazy(enum, %{}, fn f1 -> R.uniq_by(fun, f1) end)
end
@doc """
Builds a stream from an index, either starting from offset, or given by function.
May receive a function or an integer offset.
If an `offset` is given, it will emit elements from offset.
If a `function` is given, it will invoke the function with
elements from offset.
## Examples
iex> Stream.from_index() |> Enum.take(3)
[0, 1, 2]
iex> Stream.from_index(1) |> Enum.take(3)
[1, 2, 3]
iex> Stream.from_index(fn x -> x * 10 end) |> Enum.take(3)
[0, 10, 20]
"""
@doc since: "1.17.0"
@spec from_index(integer) :: Enumerable.t(integer)
@spec from_index((integer -> return_value)) :: Enumerable.t(return_value)
when return_value: term
def from_index(fun_or_offset \\ 0)
def from_index(offset) when is_integer(offset) do
unfold(offset, &{&1, &1 + 1})
end
def from_index(fun) when is_function(fun) do
unfold(0, &{fun.(&1), &1 + 1})
end
@doc """
Creates a stream where each element in the enumerable will
be wrapped in a tuple alongside its index or according to a given function.
be wrapped in a tuple alongside its index.
May receive a function or an integer offset.
If an `offset` is given, it will index from the given offset instead of from
zero.
If a `function` is given, it will index by invoking the function for each
element and index (zero-based) of the enumerable.
If an `offset` is given, we will index from the given offset instead of from zero.
## Examples
@@ -1195,24 +1153,12 @@ defmodule Stream do
iex> Enum.to_list(stream)
[{1, 3}, {2, 4}, {3, 5}]
iex> stream = Stream.with_index([1, 2, 3], fn x, index -> x + index end)
iex> Enum.to_list(stream)
[1, 3, 5]
"""
@spec with_index(Enumerable.t(), integer) :: Enumerable.t({element, integer})
@spec with_index(Enumerable.t(), (element, index -> return_value)) :: Enumerable.t(return_value)
when return_value: term
def with_index(enum, fun_or_offset \\ 0)
def with_index(enum, offset) when is_integer(offset) do
@spec with_index(Enumerable.t(), integer) :: Enumerable.t()
def with_index(enum, offset \\ 0) when is_integer(offset) do
lazy(enum, offset, fn f1 -> R.with_index(f1) end)
end
def with_index(enum, fun) when is_function(fun, 2) do
lazy(enum, 0, fn f1 -> R.with_index(fun, f1) end)
end
## Combiners
@doc """
@@ -1254,11 +1200,6 @@ defmodule Stream do
@doc """
Zips two enumerables together, lazily.
Because a list of two-element tuples with atoms as the first
tuple element is a keyword list (`Keyword`), zipping a first `Stream`
of atoms with a second `Stream` of any kind creates a `Stream`
that generates a keyword list.
The zipping finishes as soon as either enumerable completes.
## Examples
@@ -1267,8 +1208,6 @@ defmodule Stream do
iex> cycle = Stream.cycle([:a, :b, :c])
iex> Stream.zip(concat, cycle) |> Enum.to_list()
[{1, :a}, {2, :b}, {3, :c}, {4, :a}, {5, :b}, {6, :c}]
iex> Stream.zip(cycle, concat) |> Enum.to_list()
[a: 1, b: 2, c: 3, a: 4, b: 5, c: 6]
"""
@spec zip(Enumerable.t(), Enumerable.t()) :: Enumerable.t()
@@ -1572,15 +1511,13 @@ defmodule Stream do
end
@doc """
Emits a sequence of values, starting with `start_value`.
Successive values are generated by calling `next_fun`
on the previous value.
Emits a sequence of values, starting with `start_value`. Successive
values are generated by calling `next_fun` on the previous value.
## Examples
iex> Stream.iterate(1, &(&1 * 2)) |> Enum.take(5)
[1, 2, 4, 8, 16]
iex> Stream.iterate(0, &(&1 + 1)) |> Enum.take(5)
[0, 1, 2, 3, 4]
"""
@spec iterate(element, (element -> element)) :: Enumerable.t()
+1 -9
View File
@@ -238,19 +238,11 @@ defmodule Stream.Reducers do
end
end
defmacro with_index(fun) do
defmacro with_index(fun \\ nil) do
quote do
fn entry, acc(head, counter, tail) ->
next_with_acc(unquote(fun), {entry, counter}, head, counter + 1, tail)
end
end
end
defmacro with_index(callback, fun) do
quote do
fn entry, acc(head, counter, tail) ->
next_with_acc(unquote(fun), unquote(callback).(entry, counter), head, counter + 1, tail)
end
end
end
end
+107 -174
View File
@@ -167,7 +167,7 @@ defmodule String do
the locale, it is not taken into account by this module.
In general, the functions in this module rely on the Unicode
Standard, but do not contain any of the locale specific behavior.
Standard, but do not contain any of the locale specific behaviour.
More information about graphemes can be found in the [Unicode
Standard Annex #29](https://www.unicode.org/reports/tr29/).
@@ -201,9 +201,6 @@ defmodule String do
iex> eacute
233
See the [*Patterns and Guards* guide](patterns-and-guards.md) and the documentation for
[`<<>>`](`<<>>/1`) for more information on binary pattern matching.
You can also fully convert a string into a list of integer code points,
known as "charlists" in Elixir, by calling `String.to_charlist/1`:
@@ -230,7 +227,7 @@ defmodule String do
to the definition of the encoding) is encountered, only one
code point needs to be rejected.
This module relies on this behavior to ignore such invalid
This module relies on this behaviour to ignore such invalid
characters. For example, `length/1` will return
a correct result even if an invalid code point is fed into it.
@@ -396,9 +393,6 @@ defmodule String do
When the pattern used is a regular expression, the string is
split using `Regex.split/3`.
If the pattern cannot be found, a list containing the original
string will be returned.
## Options
* `:parts` (positive integer or `:infinity`) - the string
@@ -464,11 +458,6 @@ defmodule String do
iex> String.split("abc", "", parts: 3)
["", "a", "bc"]
Splitting on an non-existing pattern returns the original string:
iex> String.split("abc", ",")
["abc"]
Be aware that this function can split within or across grapheme boundaries.
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:
@@ -1484,7 +1473,7 @@ defmodule String do
The `replacement` may be a string or a function that receives the matched
pattern and must return the replacement as a string or iodata.
By default it replaces all occurrences but this behavior can be controlled
By default it replaces all occurrences but this behaviour can be controlled
through the `:global` option; see the "Options" section below.
## Options
@@ -1855,7 +1844,7 @@ defmodule String do
** (FunctionClauseError) no function clause matching in String.valid?/2
"""
@spec valid?(t, :default | :fast_ascii) :: boolean
@spec valid?(t) :: boolean
def valid?(string, algorithm \\ :default)
def valid?(<<string::binary>>, :default), do: valid_utf8?(string)
@@ -1913,21 +1902,20 @@ defmodule String do
"nem rán bERROR! bề"
"""
@doc since: "1.16.0"
@spec replace_invalid(binary, t) :: t
def replace_invalid(bytes, replacement \\ "�")
when is_binary(bytes) and is_binary(replacement) do
do_replace_invalid(bytes, replacement, <<>>)
end
# Valid ASCII (for better average speed)
defp do_replace_invalid(<<ascii::8, next::8, _::binary>> = rest, rep, acc)
defp do_replace_invalid(<<ascii::8, next::8, _::bytes>> = rest, rep, acc)
when ascii in 0..127 and replace_invalid_is_next(next) do
<<_::8, rest::binary>> = rest
<<_::8, rest::bytes>> = rest
do_replace_invalid(rest, rep, acc <> <<ascii::8>>)
end
# Valid UTF-8
defp do_replace_invalid(<<grapheme::utf8, rest::binary>>, rep, acc) do
defp do_replace_invalid(<<grapheme::utf8, rest::bytes>>, rep, acc) do
do_replace_invalid(rest, rep, acc <> <<grapheme::utf8>>)
end
@@ -1937,13 +1925,9 @@ defmodule String do
acc <> rep
end
defp do_replace_invalid(
<<0b1110::4, i::4, 0b10::2, ii::6, next::8, _::binary>> = rest,
rep,
acc
)
defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6, next::8, _::bytes>> = rest, rep, acc)
when replace_invalid_ii_of_iii(i, ii) and replace_invalid_is_next(next) do
<<_::16, rest::binary>> = rest
<<_::16, rest::bytes>> = rest
do_replace_invalid(rest, rep, acc <> rep)
end
@@ -1954,12 +1938,12 @@ defmodule String do
end
defp do_replace_invalid(
<<0b11110::5, i::3, 0b10::2, ii::6, next::8, _::binary>> = rest,
<<0b11110::5, i::3, 0b10::2, ii::6, next::8, _::bytes>> = rest,
rep,
acc
)
when replace_invalid_ii_of_iv(i, ii) and replace_invalid_is_next(next) do
<<_::16, rest::binary>> = rest
<<_::16, rest::bytes>> = rest
do_replace_invalid(rest, rep, acc <> rep)
end
@@ -1970,17 +1954,17 @@ defmodule String do
end
defp do_replace_invalid(
<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, next::8, _::binary>> = rest,
<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, next::8, _::bytes>> = rest,
rep,
acc
)
when replace_invalid_iii_of_iv(i, ii, iii) and replace_invalid_is_next(next) do
<<_::24, rest::binary>> = rest
<<_::24, rest::bytes>> = rest
do_replace_invalid(rest, rep, acc <> rep)
end
# Everything else
defp do_replace_invalid(<<_, rest::binary>>, rep, acc),
defp do_replace_invalid(<<_, rest::bytes>>, rep, acc),
do: do_replace_invalid(rest, rep, acc <> rep)
# Final
@@ -2106,27 +2090,12 @@ defmodule String do
end
end
@doc """
Returns the size (in bytes) of the next grapheme.
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.
## Examples
iex> String.next_grapheme_size("olá")
{1, "lá"}
iex> String.next_grapheme_size("")
nil
"""
@doc false
@deprecated "Use String.next_grapheme/1 instead"
@spec next_grapheme_size(t) :: {pos_integer, t} | nil
def next_grapheme_size(string) when is_binary(string) do
case :unicode_util.gc(string) do
[gc] -> {grapheme_byte_size(gc), <<>>}
[gc, rest] -> {grapheme_byte_size(gc), rest}
[gc | rest] -> {grapheme_byte_size(gc), rest}
[] -> nil
{:error, <<_, rest::bits>>} -> {1, rest}
@@ -2269,15 +2238,13 @@ defmodule String do
@doc """
Returns a substring starting at the offset `start`, and of the given `length`.
This function works on Unicode graphemes. For example, slicing the first
three characters of the string "héllo" will return "hél", which internally
is represented by more than three bytes. Use `String.byte_slice/3` if you
want to slice by a given number of bytes, while respecting the codepoint
boundaries. If you want to work on raw bytes, check `Kernel.binary_part/3`
or `Kernel.binary_slice/3` instead.
If the offset is greater than string length, then it returns `""`.
Remember this function works with Unicode graphemes and considers
the slices to represent grapheme offsets. If you want to split
on raw bytes, check `Kernel.binary_part/3` or `Kernel.binary_slice/3`
instead.
## Examples
iex> String.slice("elixir", 1, 3)
@@ -2336,13 +2303,6 @@ defmodule String do
Returns a substring from the offset given by the start of the
range to the offset given by the end of the range.
This function works on Unicode graphemes. For example, slicing the first
three characters of the string "héllo" will return "hél", which internally
is represented by more than three bytes. Use `String.byte_slice/3` if you
want to slice by a given number of bytes, while respecting the codepoint
boundaries. If you want to work on raw bytes, check `Kernel.binary_part/3`
or `Kernel.binary_slice/3` instead.
If the start of the range is not a valid offset for the given
string or if the range is in reverse order, returns `""`.
@@ -2350,6 +2310,11 @@ defmodule String do
is traversed first in order to convert the negative indices into
positive ones.
Remember this function works with Unicode graphemes and considers
the slices to represent grapheme offsets. If you want to split
on raw bytes, check `Kernel.binary_part/3` or
`Kernel.binary_slice/2` instead
## Examples
iex> String.slice("elixir", 1..3)
@@ -2517,116 +2482,6 @@ defmodule String do
defp split_bytes([], acc, _), do: {acc, 0}
defp split_bytes([head | tail], acc, count), do: split_bytes(tail, head + acc, count - 1)
@doc """
Returns a substring starting at (or after) `start_bytes` and of at most
the given `size_bytes`.
This function works on bytes and then adjusts the string to eliminate
truncated codepoints. This is useful when you have a string and you need
to guarantee it does not exceed a certain amount of bytes.
If the offset is greater than the number of bytes in the string, then it
returns `""`. Similar to `String.slice/2`, a negative `start_bytes`
will be adjusted to the end of the string (but in bytes).
This function does not guarantee the string won't have invalid codepoints,
it only guarantees to remove truncated codepoints immediately at the beginning
or the end of the slice.
## Examples
Consider the string "héllo". Let's see its representation:
iex> inspect("héllo", binaries: :as_binaries)
"<<104, 195, 169, 108, 108, 111>>"
Although the string has 5 characters, it is made of 6 bytes. Now imagine
we want to get only the first two bytes. To do so, let's use `binary_slice/3`,
which is unaware of codepoints:
iex> binary_slice("héllo", 0, 2)
<<104, 195>>
As you can see, this operation is unsafe and returns an invalid string.
That's because we cut the string in the middle of the bytes representing
"é". On the other hand, we could use `String.slice/3`:
iex> String.slice("héllo", 0, 2)
"hé"
While the above is correct, it has 3 bytes. If you have a requirement where
you need *at most* 2 bytes, the result would also be invalid. In such scenarios,
you can use this function, which will slice the given bytes, but clean up
the truncated codepoints:
iex> String.byte_slice("héllo", 0, 2)
"h"
Truncated codepoints at the beginning are also cleaned up:
iex> String.byte_slice("héllo", 2, 3)
"llo"
Note that, if you want to work on raw bytes, then you must use `binary_slice/3`
instead.
"""
@doc since: "1.17.0"
@spec byte_slice(t, integer, non_neg_integer) :: t
def byte_slice(string, start_bytes, size_bytes)
when is_binary(string) and is_integer(start_bytes) and is_integer(size_bytes) and
size_bytes >= 0 do
total = byte_size(string)
start_bytes = if start_bytes < 0, do: max(total + start_bytes, 0), else: start_bytes
if start_bytes < total do
:erlang.binary_part(string, start_bytes, total - start_bytes)
|> invalid_prefix()
|> invalid_suffix(size_bytes)
else
""
end
end
defp invalid_prefix(<<0b10::2, _::6, rest::binary>>), do: invalid_prefix(rest)
defp invalid_prefix(rest), do: rest
defp invalid_suffix(string, size) do
last = invalid_suffix(string, min(size, byte_size(string)) - 1, 0)
:erlang.binary_part(string, 0, last)
end
defp invalid_suffix(string, last, truncated) when last >= 0 do
byte = :binary.at(string, last)
cond do
# ASCII byte, discard all truncated entries
byte <= 127 ->
last + 1
# In the middle of a codepoint
byte <= 191 ->
invalid_suffix(string, last - 1, truncated + 1)
# 2 bytes codepoint start
byte <= 223 ->
if truncated == 1, do: last + truncated + 1, else: last
# 3 bytes codepoint start
byte <= 239 ->
if truncated == 2, do: last + truncated + 1, else: last
# 4 bytes codepoint start
byte <= 247 ->
if truncated == 3, do: last + truncated + 1, else: last
# Invalid codepoint, discard it, stop checking
true ->
last + 1
end
end
defp invalid_suffix(_string, _last, _truncated), do: 0
@doc """
Returns `true` if `string` starts with any of the prefixes given.
@@ -3078,13 +2933,74 @@ defmodule String do
@spec jaro_distance(t, t) :: float
def jaro_distance(string1, string2)
def jaro_distance(string, string) when is_binary(string), do: 1.0
def jaro_distance(string, string), do: 1.0
def jaro_distance(_string, ""), do: 0.0
def jaro_distance("", _string), do: 0.0
def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
# TODO: Replace by :string.jaro_similarity/2 when we require Erlang/OTP 27+
:elixir_utils.jaro_similarity(string1, string2)
{chars1, len1} = graphemes_and_length(string1)
{chars2, len2} = graphemes_and_length(string2)
case match(chars1, len1, chars2, len2) do
{0, _trans} ->
0.0
{comm, trans} ->
(comm / len1 + comm / len2 + (comm - trans) / comm) / 3
end
end
defp match(chars1, len1, chars2, len2) do
if len1 < len2 do
match(chars1, chars2, div(len2, 2) - 1)
else
match(chars2, chars1, div(len1, 2) - 1)
end
end
defp match(chars1, chars2, lim) do
match(chars1, chars2, {0, lim}, {0, 0, -1}, 0)
end
defp match([char | rest], chars, range, state, idx) do
{chars, state} = submatch(char, chars, range, state, idx)
case range do
{lim, lim} -> match(rest, tl(chars), range, state, idx + 1)
{pre, lim} -> match(rest, chars, {pre + 1, lim}, state, idx + 1)
end
end
defp match([], _, _, {comm, trans, _}, _), do: {comm, trans}
defp submatch(char, chars, {pre, _} = range, state, idx) do
case detect(char, chars, range) do
nil ->
{chars, state}
{subidx, chars} ->
{chars, proceed(state, idx - pre + subidx)}
end
end
defp detect(char, chars, {pre, lim}) do
detect(char, chars, pre + 1 + lim, 0, [])
end
defp detect(_char, _chars, 0, _idx, _acc), do: nil
defp detect(_char, [], _lim, _idx, _acc), do: nil
defp detect(char, [char | rest], _lim, idx, acc), do: {idx, Enum.reverse(acc, [nil | rest])}
defp detect(char, [other | rest], lim, idx, acc),
do: detect(char, rest, lim - 1, idx + 1, [other | acc])
defp proceed({comm, trans, former}, current) do
if current < former do
{comm + 1, trans + 1, current}
else
{comm + 1, trans, current}
end
end
@doc """
@@ -3119,6 +3035,7 @@ defmodule String do
codepoint_byte_size: 1,
grapheme_byte_size: 1,
grapheme_to_binary: 1,
graphemes_and_length: 1,
reverse_characters_to_binary: 1}
defp byte_size_unicode(binary) when is_binary(binary), do: byte_size(binary)
@@ -3155,6 +3072,22 @@ defmodule String do
defp grapheme_byte_size([], acc),
do: acc
defp graphemes_and_length(string),
do: graphemes_and_length(string, [], 0)
defp graphemes_and_length(string, acc, length) do
case :unicode_util.gc(string) do
[gc | rest] ->
graphemes_and_length(rest, [gc | acc], length + 1)
[] ->
{:lists.reverse(acc), length}
{:error, <<byte, rest::bits>>} ->
graphemes_and_length(rest, [<<byte>> | acc], length + 1)
end
end
defp reverse_characters_to_binary(acc),
do: acc |> :lists.reverse() |> :unicode.characters_to_binary()
end
+1 -1
View File
@@ -57,6 +57,6 @@ end
defimpl String.Chars, for: Float do
def to_string(term) do
:erlang.float_to_binary(term, [:short])
IO.iodata_to_binary(:io_lib_format.fwrite_g(term))
end
end
+2 -4
View File
@@ -595,7 +595,7 @@ defmodule Supervisor do
@typedoc "The supervisor reference."
@type supervisor :: pid | name | {atom, node}
@typedoc "Options given to `start_link/2` and `init/2`."
@typedoc "Options given to `start_link/2` and `c:init/1`."
@type init_option ::
{:strategy, strategy}
| {:max_restarts, non_neg_integer}
@@ -998,9 +998,7 @@ defmodule Supervisor do
def start_child(supervisor, args) when is_list(args) do
IO.warn_once(
{__MODULE__, :start_child},
fn ->
"Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead"
end,
"Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead",
_stacktrace_drop_levels = 2
)
+45 -48
View File
@@ -42,7 +42,7 @@ defmodule System do
* `system_time/0` - the VM view of the `os_time/0`. The system time and operating
system time may not match in case of time warps although the VM works towards
aligning them. This time is not monotonic (i.e., it may decrease)
as its behavior is configured [by the VM time warp
as its behaviour is configured [by the VM time warp
mode](https://www.erlang.org/doc/apps/erts/time_correction.html#Time_Warp_Modes);
* `monotonic_time/0` - a monotonically increasing time provided
@@ -305,6 +305,22 @@ defmodule System do
:elixir_config.get(:no_halt)
end
@doc """
Waits until the system boots.
Calling this function blocks until all of ARGV is processed.
Inside a release, this means the boot script and then ARGV
have been processed. This is only useful for those implementing
custom shells/consoles on top of Elixir.
However, be careful to not invoke this command from within
the process that is processing the command line arguments,
as doing so would lead to a deadlock.
"""
@doc since: "1.15.0"
@spec wait_until_booted() :: :ok
defdelegate wait_until_booted(), to: :elixir_config
@doc """
Current working directory.
@@ -506,7 +522,7 @@ defmodule System do
in case trapping exists is not supported by the current OS.
The first time a signal is trapped, it will override the
default behavior from the operating system. If the same
default behaviour from the operating system. If the same
signal is trapped multiple times, subsequent functions
given to `trap_signal` will execute *first*. In other
words, you can consider each function is prepended to
@@ -520,7 +536,7 @@ defmodule System do
* `:sigusr1` - halts the VM via status code of 1
Therefore, if you add traps to the signals above, the
default behavior above will be executed after all user
default behaviour above will be executed after all user
signals.
## Implementation notes
@@ -939,12 +955,6 @@ defmodule System do
@doc since: "1.12.0"
@spec shell(binary, keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
def shell(command, opts \\ []) when is_binary(command) do
command |> String.trim() |> do_shell(opts)
end
defp do_shell("", _opts), do: {"", 0}
defp do_shell(command, opts) do
assert_no_null_byte!(command, "System.shell/2")
{close_stdin?, opts} = Keyword.pop(opts, :close_stdin, false)
@@ -1053,11 +1063,7 @@ defmodule System do
* `:arg0` - sets the command arg0
* `:stderr_to_stdout` - redirects stderr to stdout when `true`, no effect
if `use_stdio` is `false`.
* `:use_stdio` - `true` by default, setting it to false allows direct
interaction with the terminal from the callee
* `:stderr_to_stdout` - redirects stderr to stdout when `true`
* `:parallelism` - when `true`, the VM will schedule port tasks to improve
parallelism in the system. If set to `false`, the VM will try to perform
@@ -1118,10 +1124,8 @@ defmodule System do
end
defp do_cmd(port_init, base_opts, opts) do
{use_stdio?, opts} = Keyword.pop(opts, :use_stdio, true)
{into, line, opts} =
cmd_opts(opts, [:exit_status, :binary, :hide] ++ base_opts, "", false, use_stdio?)
cmd_opts(opts, [:use_stdio, :exit_status, :binary, :hide] ++ base_opts, "", false)
{initial, fun} = Collectable.into(into)
@@ -1170,41 +1174,36 @@ defmodule System do
end
end
defp cmd_opts([{:into, any} | t], opts, _into, line, stdio?),
do: cmd_opts(t, opts, any, line, stdio?)
defp cmd_opts([{:into, any} | t], opts, _into, line),
do: cmd_opts(t, opts, any, line)
defp cmd_opts([{:cd, bin} | t], opts, into, line, stdio?) when is_binary(bin),
do: cmd_opts(t, [{:cd, bin} | opts], into, line, stdio?)
defp cmd_opts([{:cd, bin} | t], opts, into, line) when is_binary(bin),
do: cmd_opts(t, [{:cd, bin} | opts], into, line)
defp cmd_opts([{:arg0, bin} | t], opts, into, line, stdio?) when is_binary(bin),
do: cmd_opts(t, [{:arg0, bin} | opts], into, line, stdio?)
defp cmd_opts([{:arg0, bin} | t], opts, into, line) when is_binary(bin),
do: cmd_opts(t, [{:arg0, bin} | opts], into, line)
defp cmd_opts([{:stderr_to_stdout, true} | t], opts, into, line, true),
do: cmd_opts(t, [:stderr_to_stdout | opts], into, line, true)
defp cmd_opts([{:stderr_to_stdout, true} | t], opts, into, line),
do: cmd_opts(t, [:stderr_to_stdout | opts], into, line)
defp cmd_opts([{:stderr_to_stdout, true} | _], _opts, _into, _line, false),
do: raise(ArgumentError, "cannot use \"stderr_to_stdout: true\" and \"use_stdio: false\"")
defp cmd_opts([{:stderr_to_stdout, false} | t], opts, into, line),
do: cmd_opts(t, opts, into, line)
defp cmd_opts([{:stderr_to_stdout, false} | t], opts, into, line, stdio?),
do: cmd_opts(t, opts, into, line, stdio?)
defp cmd_opts([{:parallelism, bool} | t], opts, into, line) when is_boolean(bool),
do: cmd_opts(t, [{:parallelism, bool} | opts], into, line)
defp cmd_opts([{:parallelism, bool} | t], opts, into, line, stdio?) when is_boolean(bool),
do: cmd_opts(t, [{:parallelism, bool} | opts], into, line, stdio?)
defp cmd_opts([{:env, enum} | t], opts, into, line),
do: cmd_opts(t, [{:env, validate_env(enum)} | opts], into, line)
defp cmd_opts([{:env, enum} | t], opts, into, line, stdio?),
do: cmd_opts(t, [{:env, validate_env(enum)} | opts], into, line, stdio?)
defp cmd_opts([{:lines, max_line_length} | t], opts, into, _line, stdio?)
defp cmd_opts([{:lines, max_line_length} | t], opts, into, _line)
when is_integer(max_line_length) and max_line_length > 0,
do: cmd_opts(t, [{:line, max_line_length} | opts], into, true, stdio?)
do: cmd_opts(t, [{:line, max_line_length} | opts], into, true)
defp cmd_opts([{key, val} | _], _opts, _into, _line, _stdio?),
defp cmd_opts([{key, val} | _], _opts, _into, _line),
do: raise(ArgumentError, "invalid option #{inspect(key)} with value #{inspect(val)}")
defp cmd_opts([], opts, into, line, stdio?) do
opt = if stdio?, do: :use_stdio, else: :nouse_stdio
{into, line, [opt | opts]}
end
defp cmd_opts([], opts, into, line),
do: {into, line, opts}
defp validate_env(enum) do
Enum.map(enum, fn
@@ -1239,7 +1238,7 @@ defmodule System do
This time is monotonically increasing and starts in an unspecified
point in time.
"""
@spec monotonic_time(time_unit | :native) :: integer
@spec monotonic_time(time_unit) :: integer
def monotonic_time(unit) do
:erlang.monotonic_time(normalize_time_unit(unit))
end
@@ -1265,7 +1264,7 @@ defmodule System do
case of time warps although the VM works towards aligning
them. This time is not monotonic.
"""
@spec system_time(time_unit | :native) :: integer
@spec system_time(time_unit) :: integer
def system_time(unit) do
:erlang.system_time(normalize_time_unit(unit))
end
@@ -1316,7 +1315,7 @@ defmodule System do
`monotonic_time/1`), gives the Erlang system time that corresponds
to that monotonic time.
"""
@spec time_offset(time_unit | :native) :: integer
@spec time_offset(time_unit) :: integer
def time_offset(unit) do
:erlang.time_offset(normalize_time_unit(unit))
end
@@ -1442,10 +1441,8 @@ defmodule System do
defp warn(unit, replacement_unit) do
IO.warn_once(
{__MODULE__, unit},
fn ->
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
":second, :millisecond, :microsecond, :nanosecond, or a positive integer"
end,
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
_stacktrace_drop_levels = 4
)
+6 -20
View File
@@ -135,14 +135,14 @@ defmodule Task do
With `Task.Supervisor`, it is easy to dynamically start tasks across nodes:
# First on the remote node named :remote@local
# On the remote node named :remote@local
Task.Supervisor.start_link(name: MyApp.DistSupervisor)
# Then on the local client node
# On the client
supervisor = {MyApp.DistSupervisor, :remote@local}
Task.Supervisor.async(supervisor, MyMod, :my_fun, [arg1, arg2, arg3])
Note that, as above, when working with distributed tasks, one should use the
Note that, when working with distributed tasks, one should use the
`Task.Supervisor.async/5` function that expects explicit module, function,
and arguments, instead of `Task.Supervisor.async/3` that works with anonymous
functions. That's because anonymous functions expect the same module version
@@ -293,17 +293,6 @@ defmodule Task do
"""
@opaque ref :: reference()
@typedoc """
Options given to `async_stream` functions.
"""
@typedoc since: "1.17.0"
@type async_stream_option ::
{:max_concurrency, pos_integer()}
| {:ordered, boolean()}
| {:timeout, timeout()}
| {:on_timeout, :exit | :kill_task}
| {:zip_input_on_exit, boolean()}
defguardp is_timeout(timeout)
when timeout == :infinity or (is_integer(timeout) and timeout >= 0)
@@ -692,8 +681,7 @@ defmodule Task do
example above.
"""
@doc since: "1.4.0"
@spec async_stream(Enumerable.t(), module, atom, [term], [async_stream_option]) ::
Enumerable.t()
@spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t()
def async_stream(enumerable, module, function_name, args, options \\ [])
when is_atom(module) and is_atom(function_name) and is_list(args) do
build_stream(enumerable, {module, function_name, args}, options)
@@ -722,15 +710,13 @@ defmodule Task do
See `async_stream/5` for discussion, options, and more examples.
"""
@doc since: "1.4.0"
@spec async_stream(Enumerable.t(), (term -> term), [async_stream_option]) :: Enumerable.t()
@spec async_stream(Enumerable.t(), (term -> term), keyword) :: Enumerable.t()
def async_stream(enumerable, fun, options \\ [])
when is_function(fun, 1) and is_list(options) do
build_stream(enumerable, fun, options)
end
defp build_stream(enumerable, fun, options) do
options = Task.Supervised.validate_stream_options(options)
fn acc, acc_fun ->
owner = get_owner(self())
@@ -1195,7 +1181,7 @@ defmodule Task do
given time. All other tasks will have been shut down using
the `Task.shutdown/2` call.
As a convenience, you can achieve a similar behavior to above
As a convenience, you can achieve a similar behaviour to above
by specifying the `:on_timeout` option to be `:kill_task` (or
`:ignore`). See `Task.await_many/2` if you would rather exit
the caller process on timeout.
+25 -56
View File
@@ -117,9 +117,7 @@ defmodule Task.Supervised do
starter: get_from(owner),
function: fun,
args: args,
reason: {log_value(kind, value), __STACKTRACE__},
# TODO use Process.get_label/0 when we require Erlang/OTP 27+
process_label: Process.get(:"$process_label", :undefined)
reason: {log_value(kind, value), __STACKTRACE__}
}
},
%{
@@ -149,27 +147,16 @@ defmodule Task.Supervised do
starter: starter,
function: fun,
args: args,
reason: reason,
process_label: process_label
reason: reason
}
}) do
message =
~c"** Started from ~p~n" ++
~c"** Task ~p terminating~n" ++
~c"** Started from ~p~n" ++
~c"** When function == ~p~n" ++
~c"** arguments == ~p~n" ++ ~c"** Reason for termination == ~n" ++ ~c"** ~p~n"
terms = [name, fun, args, get_reason(reason)]
{message, terms} =
case process_label do
:undefined -> {message, terms}
_ -> {~c"** Process Label == ~p~n" ++ message, [process_label | terms]}
end
message =
~c"** Task ~p terminating~n" ++ message
{message, [starter | terms]}
{message, [starter, name, fun, args, get_reason(reason)]}
end
defp get_from({node, pid_or_name, _pid}) when node == node(), do: pid_or_name
@@ -202,36 +189,18 @@ defmodule Task.Supervised do
## Stream
def validate_stream_options(options) do
max_concurrency = Keyword.get_lazy(options, :max_concurrency, &System.schedulers_online/0)
on_timeout = Keyword.get(options, :on_timeout, :exit)
timeout = Keyword.get(options, :timeout, 5000)
ordered = Keyword.get(options, :ordered, true)
zip_input_on_exit = Keyword.get(options, :zip_input_on_exit, false)
def stream(enumerable, acc, reducer, callers, mfa, options, spawn) do
next = &Enumerable.reduce(enumerable, &1, fn x, acc -> {:suspend, [x | acc]} end)
max_concurrency = Keyword.get(options, :max_concurrency, System.schedulers_online())
unless is_integer(max_concurrency) and max_concurrency > 0 do
raise ArgumentError, ":max_concurrency must be an integer greater than zero"
end
unless on_timeout in [:exit, :kill_task] do
raise ArgumentError, ":on_timeout must be either :exit or :kill_task"
end
unless (is_integer(timeout) and timeout >= 0) or timeout == :infinity do
raise ArgumentError, ":timeout must be either a positive integer or :infinity"
end
%{
max_concurrency: max_concurrency,
on_timeout: on_timeout,
timeout: timeout,
ordered: ordered,
zip_input_on_exit: zip_input_on_exit
}
end
def stream(enumerable, acc, reducer, callers, mfa, options, spawn) when is_map(options) do
next = &Enumerable.reduce(enumerable, &1, fn x, acc -> {:suspend, [x | acc]} end)
ordered? = Keyword.get(options, :ordered, true)
timeout = Keyword.get(options, :timeout, 5000)
on_timeout = Keyword.get(options, :on_timeout, :exit)
zip_input_on_exit? = Keyword.get(options, :zip_input_on_exit, false)
parent = self()
{:trap_exit, trap_exit?} = Process.info(self(), :trap_exit)
@@ -243,7 +212,7 @@ defmodule Task.Supervised do
{monitor_pid, monitor_ref} =
Process.spawn(
fn -> stream_monitor(parent, spawn, trap_exit?, options.timeout) end,
fn -> stream_monitor(parent, spawn, trap_exit?, timeout) end,
spawn_opts
)
@@ -252,21 +221,21 @@ defmodule Task.Supervised do
# about our reference to it.
send(monitor_pid, {parent, monitor_ref})
config =
Map.merge(
options,
%{
reducer: reducer,
monitor_pid: monitor_pid,
monitor_ref: monitor_ref,
callers: callers,
mfa: mfa
}
)
config = %{
reducer: reducer,
monitor_pid: monitor_pid,
monitor_ref: monitor_ref,
ordered: ordered?,
timeout: timeout,
on_timeout: on_timeout,
zip_input_on_exit: zip_input_on_exit?,
callers: callers,
mfa: mfa
}
stream_reduce(
acc,
options.max_concurrency,
max_concurrency,
_spawned = 0,
_delivered = 0,
_waiting = %{},
+9 -68
View File
@@ -77,12 +77,6 @@ defmodule Task.Supervisor do
DynamicSupervisor.option()
| DynamicSupervisor.init_option()
@typedoc """
Options given to `async_stream` and `async_stream_nolink` functions.
"""
@typedoc since: "1.17.0"
@type async_stream_option :: Task.async_stream_option() | {:shutdown, Supervisor.shutdown()}
@doc false
def child_spec(opts) when is_list(opts) do
id =
@@ -362,14 +356,8 @@ defmodule Task.Supervisor do
"""
@doc since: "1.4.0"
@spec async_stream(
Supervisor.supervisor(),
Enumerable.t(),
module,
atom,
[term],
[async_stream_option]
) :: Enumerable.t()
@spec async_stream(Supervisor.supervisor(), Enumerable.t(), module, atom, [term], keyword) ::
Enumerable.t()
def async_stream(supervisor, enumerable, module, function, args, options \\ [])
when is_atom(module) and is_atom(function) and is_list(args) do
build_stream(supervisor, :link, enumerable, {module, function, args}, options)
@@ -386,12 +374,8 @@ defmodule Task.Supervisor do
See `async_stream/6` for discussion, options, and examples.
"""
@doc since: "1.4.0"
@spec async_stream(
Supervisor.supervisor(),
Enumerable.t(),
(term -> term),
[async_stream_option]
) :: Enumerable.t()
@spec async_stream(Supervisor.supervisor(), Enumerable.t(), (term -> term), keyword) ::
Enumerable.t()
def async_stream(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do
build_stream(supervisor, :link, enumerable, fun, options)
end
@@ -413,14 +397,14 @@ defmodule Task.Supervisor do
module,
atom,
[term],
[async_stream_option]
keyword
) :: Enumerable.t()
def async_stream_nolink(supervisor, enumerable, module, function, args, options \\ [])
when is_atom(module) and is_atom(function) and is_list(args) do
build_stream(supervisor, :nolink, enumerable, {module, function, args}, options)
end
@doc ~S"""
@doc """
Returns a stream that runs the given `function` concurrently on each
element in `enumerable`.
@@ -430,46 +414,10 @@ defmodule Task.Supervisor do
to `async_nolink/3`.
See `async_stream/6` for discussion and examples.
## Error handling and cleanup
Even if tasks are not linked to the caller, there is no risk of leaving dangling tasks
running after the stream halts.
Consider the following example:
Task.Supervisor.async_stream_nolink(MySupervisor, collection, fun, on_timeout: :kill_task, ordered: false)
|> Enum.each(fn
{:ok, _} -> :ok
{:exit, reason} -> raise "Task exited: #{Exception.format_exit(reason)}"
end)
If one task raises or times out:
1. the second clause gets called
2. an exception is raised
3. the stream halts
4. all ongoing tasks will be shut down
Here is another example:
Task.Supervisor.async_stream_nolink(MySupervisor, collection, fun, on_timeout: :kill_task, ordered: false)
|> Stream.filter(&match?({:ok, _}, &1))
|> Enum.take(3)
This will return the three first tasks to succeed, ignoring timeouts and errors, and shut down
every ongoing task.
Just running the stream with `Stream.run/1` on the other hand would ignore errors and process the whole stream.
"""
@doc since: "1.4.0"
@spec async_stream_nolink(
Supervisor.supervisor(),
Enumerable.t(),
(term -> term),
[async_stream_option]
) :: Enumerable.t()
@spec async_stream_nolink(Supervisor.supervisor(), Enumerable.t(), (term -> term), keyword) ::
Enumerable.t()
def async_stream_nolink(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do
build_stream(supervisor, :nolink, enumerable, fun, options)
end
@@ -612,15 +560,8 @@ defmodule Task.Supervisor do
end
defp build_stream(supervisor, link_type, enumerable, fun, options) do
shutdown = Keyword.get(options, :shutdown, 5000)
unless (is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill do
raise ArgumentError, ":shutdown must be either a positive integer or :brutal_kill"
end
options = Task.Supervised.validate_stream_options(options)
fn acc, acc_fun ->
shutdown = options[:shutdown]
owner = get_owner(self())
Task.Supervised.stream(enumerable, acc, acc_fun, get_callers(self()), fun, options, fn ->
+4 -7
View File
@@ -5,9 +5,6 @@ defmodule URI do
This module provides functions for working with URIs (for example, parsing
URIs or encoding query strings). The functions in this module are implemented
according to [RFC 3986](https://tools.ietf.org/html/rfc3986).
Additionally, the Erlang [`:uri_string` module](`:uri_string`) provides certain functionalities,
such as RFC 3986 compliant URI normalization.
"""
@doc """
@@ -238,7 +235,7 @@ defmodule URI do
{{key, value}, rest} ->
# Avoid warnings about Dict being deprecated
dict_module = String.to_atom("Dict")
dict_module = Dict
decode_query_into_dict(rest, dict_module.put(dict, key, value), encoding)
end
end
@@ -368,9 +365,9 @@ defmodule URI do
Percent-encodes all characters that require escaping in `string`.
By default, this function is meant to escape the whole URI, and
therefore it will only escape characters which are foreign in
all parts of a URI. Reserved characters (such as `:` and `/`)
or unreserved (such as letters and numbers) are not escaped.
therefore it will escape all characters which are foreign to the
URI specification. Reserved characters (such as `:` and `/`) or
unreserved (such as letters and numbers) are not escaped.
Because different components of a URI require different escaping
rules, this function also accepts a `predicate` function as an optional
@@ -117,7 +117,7 @@ def drive(%User{name: name, age: age}) when age < 18 do
end
```
While the example above is small and does not constitute an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not.
While the example above is small and does not configure an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not.
#### Refactoring
@@ -141,7 +141,7 @@ end
An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself; however, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1_048_576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation.
For these reason, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible.
For these reason, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behaviour caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible.
#### Example
@@ -321,7 +321,7 @@ When you use `map[:key]` to access a key that always exists in the map, you are
#### Example
The function `plot/1` tries to draw a graphic to represent the position of a point in a Cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D Cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys:
The function `plot/1` tries to draw a graphic to represent the position of a point in a cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys:
```elixir
defmodule Graphics do
@@ -343,7 +343,7 @@ iex> Graphics.plot(point_3d)
{5, 6, 7}
```
Given we want to plot both 2D and 3D points, the behavior above is expected. But what happens if we forget to pass a point with either `:x` or `:y`?
Given we want to plot both 2D and 3D points, the behaviour above is expected. But what happens if we forget to pass a point with either `:x` or `:y`?
```elixir
iex> bad_point = %{y: 3, z: 4}
@@ -352,7 +352,7 @@ iex> Graphics.plot(bad_point)
{nil, 3, 4}
```
The behavior above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on.
The behaviour above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on.
#### Refactoring
@@ -371,8 +371,8 @@ end
iex> Graphics.plot(point_2d)
{2, 3, nil}
iex> Graphics.plot(bad_point)
** (KeyError) key :x not found in: %{y: 3, z: 4}
graphic.ex:4: Graphics.plot/1
** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that
graphic.ex:4: Graphics.plot/1 # <= the :x key does not exist!
```
Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples.
@@ -448,7 +448,7 @@ iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "univ
#### Refactoring
To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behavior, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted:
To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behaviour, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted:
```elixir
defmodule Extract do
@@ -17,12 +17,12 @@ defmodule AlternativeInteger do
@spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error
def parse(string, options \\ []) when is_list(options) do
if Keyword.get(options, :discard_rest, false) do
Integer.parse(string)
else
case Integer.parse(string) do
{int, _rest} -> int
:error -> :error
end
else
Integer.parse(string)
end
end
end
@@ -30,11 +30,11 @@ end
```elixir
iex> AlternativeInteger.parse("13")
{13, ""}
iex> AlternativeInteger.parse("13", discard_rest: false)
{13, ""}
13
iex> AlternativeInteger.parse("13", discard_rest: true)
13
iex> AlternativeInteger.parse("13", discard_rest: false)
{13, ""}
```
#### Refactoring
@@ -75,7 +75,7 @@ This is a special case of [*Primitive obsession*](#primitive-obsession), specifi
#### Example
An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behavior in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related.
An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behaviour in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related.
```elixir
defmodule MyApp do
@@ -187,14 +187,6 @@ A common practice followed by the community is to make the non-raising version r
#### Additional remarks
This anti-pattern is of special importance to library authors and whenever writing functions that will be invoked by other developers and third-party code. Nevertheless, there are still scenarios where developers can afford to raise exceptions directly, for example:
* invalid arguments: it is expected that functions will raise for invalid arguments, as those are structural error and not semantic errors. For example, `File.read(123)` will always raise, because `123` is never a valid filename
* during tests, scripts, etc: those are common scenarios where you want your code to fail as soon as possible in case of errors. Using `!` functions, such as `File.read!/1`, allows you to do so quickly and with clear error messages
* some frameworks, such as [Phoenix](https://phoenixframework.org), allow developers to raise exceptions in their code and uses a protocol to convert these exceptions into semantic HTTP responses
This anti-pattern was formerly known as [Using exceptions for control-flow](https://github.com/lucasvegi/Elixir-Code-Smells#using-exceptions-for-control-flow).
## Primitive obsession
@@ -225,7 +217,7 @@ Another example of this anti-pattern is using floating numbers to model money an
#### Refactoring
Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remaining functions. With this modification, we can extract each field of this composite type individually when needed.
Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remainng functions. With this modification, we can extract each field of this composite type individually when needed.
```elixir
defmodule Address do
@@ -257,7 +249,7 @@ Using multi-clause functions is a powerful Elixir feature. However, some develop
#### Example
A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behavior of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them.
A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behaviour of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them.
Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated.
@@ -304,7 +296,7 @@ def update_animal(%Animal{count: count, skin: skin}) do
end
```
These functions may still be implemented with multiple clauses, as long as the clauses group related functionality. For example, `update_product` could be in practice implemented as follows:
These functions may still be implemented with multiple clauses, as long as the clauses group related funtionality. For example, `update_product` could be in practice implemented as follows:
```elixir
def update_product(%Product{count: 0}) do
@@ -339,7 +331,7 @@ iex> struct(URI.parse("/foo/bar"), path: "/bar/baz")
}
```
The difference here is that the `struct/2` function behaves precisely the same for any struct given, therefore there is no question of how the function handles different inputs. If the behavior is clear and consistent for all inputs, then the anti-pattern does not take place.
The difference here is that the `struct/2` function behaves precisely the same for any struct given, therefore there is no question of how the function handles different inputs. If the behaviour is clear and consistent for all inputs, then the anti-pattern does not take place.
## Using application configuration for libraries
@@ -310,7 +310,7 @@ iex> Counter.bump(Counter, 7)
#### Refactoring
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the life cycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
```elixir
defmodule SupervisedProcess.Application do
@@ -1,6 +1,6 @@
# alias, require, import, and use
# alias, require, and import
In order to facilitate software reuse, Elixir provides three directives (`alias`, `require`, and `import`) plus a macro called `use` summarized below:
In order to facilitate software reuse, Elixir provides three directives (`alias`, `require` and `import`) plus a macro called `use` summarized below:
```elixir
# Alias the module so it can be called as Bar instead of Foo.Bar

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