Compare commits

..
Author SHA1 Message Date
José Valim 915d6dca16 Release v1.19.6 2026-08-28 12:07:13 +02:00
José Valim a983c8c043 Fix recursion on charlist error path 2026-08-28 12:05:38 +02:00
José Valim d33fd8e413 Release v1.19.5 2026-01-09 11:57:56 +01:00
José Valim 02603b147c Do not check the position in Regex errors, closes #15033 2026-01-01 10:54:31 +01:00
José Valim e126dab368 Optimize protocol consolidation
* Allow directory listings to be pre-computed
* Do not require struct modules to be loaded

The current implementation would require us to
recompute protocols whenever a struct changed,
something that is not currently available.
Furthermore, the fact we had to load modules
naturally made consolidation more expensive.
Therefore we need a lazy type resolution,
and until we have such feature, we will only
track struct names.
2025-12-19 18:42:49 +01:00
Łukasz Samson 38547285f1 Fix parser crash on missing parentheses on expression following operator \"not in\" warning (#15011) 2025-12-12 12:14:39 +01:00
Jechol Lee 4f07670d08 Fix unnecessary recompilation when dbg_callback is modified at runtime (#15007)
When :elixir app starts, store the dbg_callback value in dbg_callback_initial.
Mix compiler now compares against dbg_callback_initial instead of dbg_callback.

This prevents unnecessary recompilation when tools like Kino modify
dbg_callback at runtime. Previously, such runtime modifications would trigger
a full recompilation of all files using dbg/2, even though the config hadn't
actually changed.

The key insight is that dbg/2 is a compile-time macro, so runtime modifications
to dbg_callback don't affect already-compiled code. Only actual config changes
(reflected in dbg_callback_initial) should trigger recompilation.

This is a more general solution than detecting specific wrapping patterns,
as it works with any tool that modifies dbg_callback at runtime.
2025-12-10 09:21:49 +01:00
Andrea Leopardi dcff0ffaea Fix spec for t:ExUnit.Callbacks.child_spec_overrides/2 (#15006) 2025-12-09 08:14:48 -05:00
José Valim 97407cde90 Do not crash on invalid metadata in deprecated backends, closes #14995 2025-12-04 11:58:18 +01:00
Jean Klingler 579fc97b97 Revert Stream transform simplification causing regression (#14993)
* Revert "Simplify transform as suspend is guaranteed to return one entry"

This reverts commit a7c008f2cd.

* Add regression test case
2025-12-04 18:06:12 +09:00
José Valim 003ef335e7 Fix #iex:break as part of multi-line prompts, closes #14992 2025-12-04 00:10:07 +01:00
José Valim 028cdbb91e Support fetching abstract code for modules compiled with Elixir v1.14 and earlier, closes #14987 2025-12-03 20:00:37 +01:00
José Valim 794441a255 Release v1.19.4 2025-11-27 16:50:26 +01:00
Eksperimental 727106beb4 Fix typos (#14951) 2025-11-27 16:49:21 +01:00
Wojtek Mach 5e45871b5e mix help app:APP: compile current Mix project (if any) (#14958)
Prior to this patch, it was impossible to get help for the current app:

    nimble_csv$ mix help app:nimble_csv
    Application nimble_csv does not exist or is not loaded

It also helps with projects with depenendencies when they haven't been
compiled yet:

    req$ mix deps.get && mix help app:finch # mix deps.compile haven't been run yet
    Application finch does not exist or is not loaded

Now it works:

    nimble_csv$ mix help app:nimble_csv
    Compiling 1 file (.ex)
    Generated nimble_csv app
    # NimbleCSV

    NimbleCSV is a small and fast parsing and dumping library.

    # NimbleCSV.RFC4180

    A CSV parser that uses comma as separator and double-quotes as escape according
    to RFC4180.

    # NimbleCSV.Spreadsheet

    A parser with spreadsheet friendly settings.
2025-11-27 15:36:13 +01:00
José Valim 0db308cbff Clarify @on_load will be called early on releases, closes #14960 2025-11-27 15:26:18 +01:00
José Valim 35a7e3aa1f Fix typo 2025-11-26 19:53:12 +01:00
José Valim 7340597020 Clarify struct and record fields, closes #14963 2025-11-26 19:52:43 +01:00
José Valim 3b6acc33d2 Fix task supervisor name 2025-11-24 18:27:17 +01:00
José Valim 2ed048acc6 Include hex in the list of escript.build special cases (#14953) 2025-11-24 15:28:33 +01:00
José Valim bf0c203d52 Skip warning on :re.import/1 2025-11-21 14:04:19 -08:00
Łukasz Samson b074e6b159 Fix ex_unit formatter crash when find_diff returns nil (#14939) 2025-11-21 05:34:16 -08:00
Jean Klingler bfe7315fbb File.cp_r reports non-existing dest dir properly (#14929) 2025-11-20 21:43:53 +09:00
Jean Klingler 28f4fdc21b Fix dialyzer warning in Code.format_string!/2 (#14928)
* Fix dialyzer warning in Code.format_string!/2

Tentative fix for https://github.com/elixir-lang/elixir/issues/14927

* Add regression test for IO.inspect/2 opts
2025-11-20 19:41:01 +09:00
José Valim ee1dc0729f Ensure parallel matches in assert propagate type information, closes #14921 2025-11-18 15:20:17 -08:00
José Valim 32e724c87c Ensure logger formatter works with chardata, closes #14921 2025-11-18 14:50:50 -08:00
José Valim 7758084211 Add --min-cycle-label to mix xref graph 2025-11-15 06:36:31 -08:00
Jean Klingler 57d3098c2a Fix since in doc for E regex modifier (#14909) 2025-11-14 07:48:38 +09:00
José Valim 427d21e410 Release v1.19.3 2025-11-13 17:33:43 +01:00
José Valim 24a188a016 Share consulting logic between compile.app and release 2025-11-13 16:11:15 +01:00
Jean Klingler 457d52c89b Add 'E' modifier to Regex for :export option (#14907) 2025-11-13 16:11:15 +01:00
José Valim 6684dbd3f2 Simplify format stacktrace handling 2025-11-13 13:10:59 +01:00
Łukasz Samson ccb2a742ad Fix crash when format_kind_reason is executed with TestModule (#14901)
Closes #14900.
2025-11-13 13:10:58 +01:00
Jean Klingler 92094c9ef1 Write ascii printable charlists as such in .app file (#14906) 2025-11-13 18:42:54 +09:00
Wojtek Mach d4d5da2ccf mix help app:APP: Always try loading apps and support Erlang apps (#14894) 2025-11-08 12:23:04 +01:00
José Valim 666c082105 &Mod.fun/arity are valid app terms, closes #14891 2025-11-08 12:23:03 +01:00
José Valim d2c9d39efe Allow forcing specific compilers 2025-11-08 11:47:24 +01:00
Jean Klingler 97d16038fe Fix how UTF8 env is stored in .app file (#14890) 2025-11-04 16:21:08 +09:00
José Valim 87cbf06ff1 Release v1.19.2 2025-11-02 11:50:21 +01:00
José Valim caf39255a4 Ensure relative MIX_BUILD_PATH is discarded on deps
Closes #14875.
2025-11-02 10:17:18 +01:00
José Valim 6440fb90a3 Ensure mix escript.install works with deps.partition, closes #14885 2025-11-02 09:50:18 +01:00
José Valim 8793c68822 Remove line breaks from deprecations table 2025-10-30 21:59:52 +01:00
Gonzalo R 49ac038d67 docs(logger): :process_label is not really ever included in the metadata as of now (#14880) 2025-10-30 17:48:20 +01:00
Leslieandwolfgang 3827b73d2d Fix struct update example in guide (#14877)
Co-authored-by: wolfgang <wolfgang.yin@gmail.com>
2025-10-30 22:53:07 +09:00
José Valim dc8a19028f Commit test changes to MIX_BUILD_PATH 2025-10-30 08:27:25 +01:00
José Valim 357c346a9f Ensure MIX_BUILD_PATH is expanded 2025-10-30 08:13:48 +01:00
Jean Klingler 6df9eb338c Document since for --no-check-cwd + fix typo (#14874) 2025-10-30 07:39:03 +09:00
José Valim fa85b95250 Pass the scm when touching compilation manifest for deps 2025-10-29 23:28:40 +01:00
José Valim a89655c434 Ensure latest version of applications are loaded after compilation 2025-10-29 14:35:23 +01:00
Jean Klingler 730fd0f824 Warn on deprecated ~~~ unary op in tokenizer (#14870) 2025-10-29 19:27:48 +09:00
José Valim 29d3b0cf3a Rely on term_to_binary/1 format to protect from incomplete file writes 2025-10-28 20:27:22 +01:00
José Valim ae94e6eb25 Add checksum to manifest files
Closes #14866.
2025-10-28 12:01:03 +01:00
José Valim 661e537cda Add --no-check-pwd to mix compile 2025-10-28 12:01:02 +01:00
José Valim 4a5c076b7f Warn if elixirc_paths is not a list of string paths, closes #14862 2025-10-28 08:11:04 +01:00
Jean Klingler 297bc8302b Fix dialyzer warning on IO.inspect :label (#14865) 2025-10-28 16:06:40 +09:00
José Valim f6413152ff Ensure flags are passed down to umbrella children
Closes #14852.
Closes #14860.
2025-10-26 13:26:45 +01:00
José Valim c2edbaf173 Raise on invalid .app files (#14861)
Since regular expressions now have regexes,
they can end-up in .app files, and then apps
can no longer boot.

This changes makes it so they fail loudly.
2025-10-24 23:56:06 +02:00
José Valim ac9a659bc6 Reset ansi escapes before newlines in Logger
Closes #14855
Closes #14841
2025-10-24 12:19:22 +02:00
José Valim cc5837caa7 Pass signatures on the side instead of module map 2025-10-23 00:18:29 +02:00
José Valim 1738ed453f Do not include signatures twice in beam files 2025-10-23 00:01:32 +02:00
José Valim 3e18aafa0a Measure and optimize writing of .beam files in the compiler
This should provide meaningful benefits when working with
containers and network mounted drives.
2025-10-22 10:47:03 +02:00
José Valim bcb73a468b Clarify CHANGELOG, closes #14844 2025-10-21 14:26:48 +02:00
José Valim 613f631487 Simplify always true and always false bdd nodes 2025-10-21 12:51:11 +02:00
José Valim f08a84af10 Make bdd functions public to aid debugging 2025-10-21 12:40:29 +02:00
José Valim b1a7afd061 Release v1.19.1 2025-10-20 17:30:38 +02:00
Travis Vander Hoop e9bf5df25a Update documentation for formatter's :excludes option (#14842) 2025-10-18 20:35:08 +02:00
José Valim 2e7bb9b47f Do not spawn partitions when all dependencies are local and ok, closes #14843 2025-10-18 19:07:28 +02:00
José Valim 83c68cecc1 Simplify computation of mismatched parts for error messages
Before we were doing:

    common = intersection(actual, expected)
    uncommon = difference(actual, common)

But the second clause is:

    actual and not (actual and expected)

Which is literally the same as:

    actual and not expected

But much faster as it avoids the large nesting of BDDs.

Closes #14836.
2025-10-18 18:54:39 +02:00
José Valim 184b724483 More optimizations for differences
* when a1 < a2
* when a1 == a2 and c2 == bottom and d2 == bottom
* when a1 == a2 and u2 == bottom
2025-10-18 18:33:18 +02:00
José Valim 47fea9580a Perform expensive operation once 2025-10-18 18:32:44 +02:00
José Valim 6a6678f337 Do not attempt to touch deleted files 2025-10-18 18:32:32 +02:00
José Valim ec87c6b111 Improve protocol violation warnings 2025-10-18 18:32:28 +02:00
José Valim 0c595489b6 Improve protocol type error to list possible root causes 2025-10-18 18:32:22 +02:00
Eric Meadows-Jönsson cdbfa09edb Fix hex upload of Elixir build without -otp- suffix (#14840)
OTP 25 is no longer supported so we didn't upload a generic build.
Instead find the oldest version instead of hardcoding it.
2025-10-17 22:21:14 +02:00
José Valim baa25991fa Do not escape dbg options, closes #14839 2025-10-17 22:15:22 +02:00
Daniil Kulchenko a5c9121a7c Fix EEx.compile_string passing invalid options to tokenize (#14835)
EEx.compile_string/2 was passing all options to tokenize/2, but
tokenize/2 only accepts tokenize_opt (:file, :line, :column,
:indentation, :trim). This caused dialyzer to correctly flag calls
with :engine or :parser_options as type errors in Elixir 1.19+.

The fix filters options before passing to tokenize/2, keeping only
the valid tokenize_opt keys, while still passing the full options
list to EEx.Compiler.compile/3 which needs :engine, :parser_options,
and custom engine options.

Fixes #14834
2025-10-16 23:49:53 +02:00
José Valim 177dc8ab59 Release v1.19.0 2025-10-16 08:52:04 +02:00
José Valim bba8835ecc Fix IEx parser fetching on mix release 2025-10-16 08:46:48 +02:00
José Valim dea8456022 Escape meta within existing quote extensions (#14832)
Closes #14829
Closes #14830
2025-10-14 13:05:44 +02:00
José Valim 544a10ccfa Update CHANGELOG 2025-10-09 10:08:12 +02:00
José Valim ee14357bcd Add --shell to mix cmd (#14827) 2025-10-09 10:07:18 +02:00
José Valim 2fa136e60a Deal with relative paths in mix cmd, closes #14787 (#14788) 2025-10-09 09:27:16 +02:00
Eksperimental f382affce3 Fix typos in v1.19 (#14826) 2025-10-09 08:48:14 +02:00
José Valim 36190985d2 Include a hint for defimpl type checking 2025-10-08 18:27:39 +02:00
José Valim 957f84b7cd Release v1.19.0-rc.2 2025-10-07 18:10:32 +02:00
José Valim 74fab3caf4 Update parallel compiler docs, closes #14821 2025-10-07 17:54:41 +02:00
Eksperimental f1a6a4e019 Improve Kernel.ParallelCompiler warning (#14820) 2025-10-07 17:54:41 +02:00
Eksperimental c7884ca393 Elixir v1.19 introduces a warning related to structs, (#14818)
hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:

         user = some_fun()
         %User{user | name: "John Doe"}

     it is enough to write:

         %User{} = user = some_fun()
         %{user | name: "John Doe"}

Since this could be seen by new-comers to the language, offering a better user experience by avoiding abbreviations. Favoring the usage of "some_function" instead of "some_fun"
2025-10-07 17:54:41 +02:00
José Valim 6f3fb272af Add mix help app:APP, closes #14782 2025-10-07 17:47:31 +02:00
José Valim 8c14d3a818 Add newline after inspection, closes #14819 2025-10-07 17:15:24 +02:00
José Valim 9e10ea876f Update CHANGELOG 2025-10-07 15:58:51 +02:00
José Valim 64f69cfee8 Improve error message when escaping default values with custom rules in structs, closes #14817 2025-10-07 14:26:11 +02:00
José Valim 7844c98fe7 Ensure escaping works within struct fields, closes #14817 2025-10-07 13:07:42 +02:00
José Valim 519eff5e29 Do not crash on empty test unit groups, closes #14754 2025-10-07 11:19:23 +02:00
Rafał Studnicki e29c40a7e9 Add key-based partitioning to duplicate registries (#14654) 2025-10-06 19:02:01 +02:00
José Valim 9fc3708980 Fix optimizations for closed map checking (#14813) 2025-10-06 17:25:10 +02:00
Jonatan Männchen c06b6a929e Update ORT Scanner (#14594) 2025-10-06 08:39:20 +02:00
Wojtek Mach cfa3bb7429 Fix preloading modules in mix test --slowest-modules=N (#14811) 2025-10-05 21:46:49 +02:00
Art Kay 195a4cf9f7 Optimize Access.filter to eliminate intermediate list creation (#14749) 2025-10-05 15:38:42 +02:00
José Valim 8daa2a6f67 Checkpoint before verification to avoid ignore modules warnings 2025-10-05 15:33:41 +02:00
José Valim d38f331db6 Use a map to track Mix compiler state 2025-10-05 15:29:54 +02:00
José Valim f642b30e42 Release v1.19.0-rc.1 2025-10-05 12:13:11 +02:00
José Valim 23f5ade25c Release v1.19.0-rc.1 2025-10-05 12:07:50 +02:00
José Valim b0c1c35933 Fix regression on direct raise of ExUnit.AssertionError 2025-10-04 21:40:42 +02:00
José Valim 734d76a51b Store bdd leaves in a unified and compact format (#14807) 2025-10-04 13:12:38 +02:00
José Valim b2bf886884 Improve docs for test patterns 2025-10-04 13:10:41 +02:00
Jean Klingler d703e784f4 Fix inaccurate hint for disabling :test_ignore_filters (#14808) 2025-10-04 13:10:36 +02:00
José Valim 44fd817935 Use lazy bdd for all types (#14806)
We use Lazy BDDS: ternary trees (instead of binary) where the additional node
encodes a lazy union, as in "COVARIANCE AND CONTRAVARIANCE:
A FRESH LOOK AT AN OLD ISSUE", with some additional optimisations
for intersections and differences to avoid materialising unions.
2025-10-04 13:10:31 +02:00
José Valim f8dcd941ff Fix :file.pwrite return type 2025-10-01 14:44:40 +02:00
José Valim ce403643cd Include previous clause line on default errors/warnings, closes #14804 2025-10-01 08:45:01 +02:00
José Valim 668c9a8f5d Revert "Do not allow protocols to define structs nor exceptions, closes #14158"
Projects rely on this feature, therefore we have to revert
to avoid breaking changes.

This reverts commit 01c82022f2.

Closes #14803.
2025-10-01 08:21:28 +02:00
Eksperimental 3175901d89 Align Regex dotall modifier to PCRE2 (#14792) 2025-09-30 16:48:03 +02:00
José Valim a2baac915a Address regressions on 'not in' operator
Closes #14783.
2025-09-30 16:47:11 +02:00
Jonatan Kłosko 5a5e3855aa Improve user detection in lock and pubsub implementation (#14801) 2025-09-30 16:46:31 +02:00
Jonatan Kłosko cd580f679c Fix File.rename/2 race condition in lock implementation on windows (#14800) 2025-09-30 16:46:31 +02:00
José Valim 33347f107e Update CHANGELOG 2025-09-29 09:36:03 +02:00
José Valim cb15a3dd4c Convert line break error into a warning for v1.19 2025-09-29 09:26:15 +02:00
José Valim a4f98b1177 Optimize DNFs to avoid negations when possible 2025-09-28 20:01:26 +02:00
Guillaume Duboc 5b2f9c9201 Rewrite maps, tuples, lists as BDDs and improve performance (#14693) 2025-09-28 19:05:30 +02:00
Zach Daniel b969aead71 Make errors when piping expressions in IEx safer (#14795) 2025-09-28 16:18:23 +02:00
Jean Klingler bfbd7658b3 Assert scope is not match/guard when using escaped regexes (#14780) 2025-09-27 23:02:05 +09:00
José Valim 1327c0c5c3 Improve docs for shift vs add 2025-09-22 10:42:54 +02:00
Jean Klingler 64c61df2bf Warn on boot for OTP28.0 (#14732) 2025-09-18 19:10:12 +09:00
Jean Klingler 4363dffaf5 Fix Macro.escape/1 bug when :quote tuples is in the tail of a list (#14775)
Close https://github.com/elixir-lang/elixir/issues/14771
2025-09-17 07:15:52 +09:00
Jean Klingler 7be008c1b0 Bugfix: Macro.escape/1 properly escapes meta in :quote tuples (#14773)
Close #14771

Also internally renames the `op` field inside `elixir_quote`: `none -> escape`, `prune_metadata` -> `escape_and_prune`, `add_context -> quote`.
2025-09-16 18:37:16 +09:00
Jonatan Männchen 499a9c55d3 Correct builds.hex.pm Publish Condition in CI (#14772) 2025-09-15 23:14:27 +02:00
José Valim b18cdb0f7a Do not persist temporary compilation warnings, closes #14768 2025-09-14 10:45:01 +02:00
José Valim 62949b0728 Revert "Convert verification errors into diagnostics, closes #14768"
This reverts commit 2dc3d2713c.

This solution will still leave a corrupted state if
verification is manually aborted.
2025-09-14 10:44:58 +02:00
José Valim cb108d5ce5 Unify mix recompile and external resource handling
Imagine the following scenario:

1. the user changes an external resource
2. the user calls mix compile and it fails
3. the user reverts the changes to the external resource

We had already fixed this bug for `__mix_recompile__?`.
Therefore, this pull request unifies how both are handled
by touching the source file after we detect it is stale
(which is also what we do on every subsequent compilation
cycle).

Note that before we stored the result of `__mix_recompile__?`
in the checkpoint file. However, that's not needed. The checkpoint
is useful to track changes to sources that may not define any module.
But since the compilation check and external resources always
require a module, touching the file is enough.

Closes https://github.com/phoenixframework/phoenix/issues/6476.
2025-09-13 21:32:04 +02:00
José Valim 1f7ddd7acd Convert verification errors into diagnostics, closes #14768 2025-09-13 20:07:44 +02:00
José Valim 7477c9326e Expand on why we supervise, not how (#14764)
Closes #14763.
2025-09-10 22:16:01 +02:00
Jean Klingler c0e334df2a Add missing :generated to Macro.escape_opts/0 type (#14761) 2025-09-10 20:14:26 +09:00
José Valim fd9dbf8490 Update Unicode to version 17.0.0 (#14760)
This is an automated commit created by the Maintenance project
https://github.com/eksperimental/maintenance

Please read the release notes by visiting
<http://www.unicode.org/versions/Unicode17.0.0/>.
2025-09-10 09:10:32 +02:00
Jean Klingler 7f45befed6 Have mix test fail if warnings and --warnings-as-errors (#14756) 2025-09-10 07:46:45 +09:00
Nathan Long bbcd7f3094 ExUnit sets a process label for each test (#14758) 2025-09-09 21:53:24 +02:00
José Valim 51ac9bc324 Accept any enumerable in Logger.metadata/1 2025-09-09 15:31:19 +02:00
Jean Klingler c8f8d02cc0 Fix dialyzer opaqueness warnings on module attrs in OTP28 (#14755)
* Mark module attributes as generated in case they contain opaque terms

* Add and use Macro.escape(ast, generated: true)
2025-09-09 17:51:50 +09:00
Jean Klingler d0634c9188 Fix infinite loop: Enum.take/2 with negative index on empty enum (#14747) 2025-09-05 21:25:51 +09:00
Jonatan Männchen cc797de249 tighten CI secret scope and move AWS config to environment vars (#14627)
* Add `environment: release` to the "publish-to-hex" job so that only
  workflows explicitly targeting the release environment can read
  sensitive values.
* Gate the job behind `if: ${{ vars.HEX_AWS_REGION }}` to avoid noisy
  failures in forks where the variable is not configured.
* Replace `${{ secrets.HEX_AWS_REGION }}` / `${{ secrets.HEX_AWS_S3_BUCKET }}`
  references with `${{ vars.* }}`.  These are not credentials, so
  environment-level *variables* are a better fit and keep them readable
  only by jobs that declare the environment.
* Remove Fastly secrets from the job-wide `env:` block and inject them
  only into the Fastly purge step, following the principle of least
  privilege.  Other steps no longer see these tokens.

Restricting secret visibility to an environment and to the exact step
that needs them reduces the blast radius of a compromised workflow run,
blocks accidental exposure in logs of unrelated steps, and stops forks
from obtaining privileged data.
2025-09-03 22:14:58 +02:00
José Valim f56139aa2c Add closing token metadata to a.{}, closes #14682 2025-08-31 22:10:01 +02:00
José Valim 8ac8230e18 Properly handle column for 'in' in 'not in' operator
Closes #14681.
2025-08-31 22:10:00 +02:00
sabiwara 0fca9beaf7 Remove test for warning disabled on 1.19 2025-08-31 20:10:59 +09:00
Jean Klingler b3e3e8c6aa Do not consider variables from pattern in bitstring modifier (#14738) 2025-08-31 19:21:15 +09:00
José Valim 61603a8725 Update CHANGELOG 2025-08-31 11:20:33 +02:00
José Valim 44c5069573 Update CHANGELOG 2025-08-31 11:11:53 +02:00
Jesse Stimpson a96c04f260 Improve docs on iex remote shell halt behaviour (#14721) 2025-08-31 10:59:33 +02:00
Eksperimental d458fcb9f5 Fix order in ExUnit results when listing pinned variables (#14723)
The pinned variables were returned in a random order (often reversed):

     test/ex_unit_pinned_variables_order_test.exs:23
     match (=) failed
     The following variables were pinned:
       var_d = "four"
       var_c = "three"
       var_b = "two"
       var_a = "one"
     code:  assert %{a: ^var_d, b: ^var_c, c: ^var_b, d: ^var_a} = build(var_a, var_b, var_c, var_d)
     left:  %{a: ^var_d, b: ^var_c, c: ^var_b, d: ^var_a}
     right: %{a: "one", b: "two", c: "three", d: "four"}
     stacktrace:
       test/ex_unit_pinned_variables_order_test.exs:29: (test)

This fix sorts them alphabetically.

This bug was introduced in 884e93391e when the pinned vars were now accumulated in a map (instead
of a list).

A repo replicating the issue can be found here:
- https://github.com/eksperimental-debug/elixir_debug/tree/ex-unit-pinned-variables-order
- https://github.com/eksperimental-debug/elixir_debug/blob/ex-unit-pinned-variables-order/ex_unit_pinned_variables_order/test/ex_unit_pinned_variables_order_test.exs
2025-08-31 10:58:58 +02:00
José Valim d507502ecd Update bidi/line break character checks according to UX#55 2025-08-31 10:57:43 +02:00
José Valim 2bb27ea128 Only break newlines if original char is a newline 2025-08-31 10:57:43 +02:00
Lukasz Samson 6fbc6e08a0 Advance line when processing ? followed by <LF> and \<LF>
Closes #14715.
2025-08-31 10:57:43 +02:00
José Valim 71bb017b61 Raise if message in AssertionError is not a binary
Closes #14695.
2025-08-31 10:57:43 +02:00
Łukasz Samson 1fadefe25f Catch-all clause for unbalanced terminators (#14694) 2025-08-31 10:57:43 +02:00
Jean Klingler e1f34d09af Shallow-validate the return of __escape__ (#14736) 2025-08-31 15:51:46 +09:00
Jean Klingler ff21a9d601 Add __escape__/1 and use it to fix Regex escaping in OTP28.1+ (#14720)
Leverages newly added :re.import/1.
https://github.com/erlang/otp/pull/9976
2025-08-30 18:29:56 +09:00
Jean Klingler 1778bf211c Inspect ill-formed structs as maps (#14718) 2025-08-23 17:16:20 +09:00
José Valim 99f2c8f2ea Fix docs for Macro.compile_apply/4 2025-08-17 09:53:59 +02:00
José Valim 54369ba3e5 Improve docs on DynamicSupervisor blocking operations 2025-08-15 19:39:20 +02:00
Steve Cohen ba0f3935d3 Fix filtering documentation (#14705)
The filtering documentation implied that the msg attribute of the
logger event map could be a binary, but according to the erlang types
(https://www.erlang.org/doc/apps/kernel/logger.html#t:log_event/0) it
can't be a binary.

This change updates the docs with an example that comports with the
actual typing.
2025-08-08 08:43:50 +02:00
Eksperimental b566edc2ab ExUnit: Raise explaining what failed on invalid tags (#14707) 2025-08-08 08:43:49 +02:00
Chris Hicks 297f1f3edf Add options to mix format to allow excluding of files (#14702) 2025-08-06 10:05:21 +02:00
Łukasz Samson 764235f1da Fix expand crash on invalid multialias root (#14698) 2025-08-06 10:05:20 +02:00
ice_cap 8d216b87cd Update structs.md (#14683) 2025-08-04 08:59:33 +02:00
José Valim 2c7fa47ad6 Remove general catch on sigil token 2025-07-28 08:28:58 +02:00
Łukasz Samson 71e1ddc64e Return error on invalid unicode sequences (#14666) 2025-07-27 19:04:52 +02:00
José Valim f50f35fd1c Add --name-pattern option to mix test and regex support to OptionParser (#14674) 2025-07-26 19:56:05 +02:00
José Valim 5bc81aaa0a Enhance OptionParser.ParseError with available options display (#14673)
Example output:

  Expected one of:
    --count INTEGER (alias: -c)
    --debug, --no-debug (alias: -d)
    --files STRING (alias: -f) (may be given more than once)
    --verbose, --no-verbose (alias: -v)

Prompt
======

When we raise ParseError, include all of the options we could
potentially accept, alongside their types and aliases. For example,
the switches `[foo: :string, bar: :integer]` and `aliases: [b: :bar]`,
the error message should say:

    Expected one of:
      --foo STRING
      --bar INTEGER (alias: -b)

Furthermore, for types that are :keep (which default to string), you should
add:

    --bar INTEGER (alias: -b) (may be given more than once)

And boolean ones accept no arguments, so they should be written as:

    --baz, --no-baz

Sort all of them alphabetically.
2025-07-26 19:56:05 +02:00
José Valim 99cef0686c Improve ExUnit docs 2025-07-26 19:56:05 +02:00
Paul Gideon Dann 2308be941e Validate type of :deps_paths option for formatter_for_file/2 (#14669) 2025-07-26 19:56:05 +02:00
Jean Klingler 79005e370e Fix opaqueness violation in Task.Supervisor (#14656) 2025-07-18 06:43:08 +09:00
José Valim 1f75ade71f Avoid adding lists that match negations 2025-07-16 22:09:44 +02:00
José Valim a719cb3b21 Update checker to v2 as representation has changed 2025-07-16 16:39:29 +02:00
Guillaume Duboc ac901d9d27 Remove duplicate for map_difference 2025-07-16 16:20:38 +02:00
Benjamin Milde f9966230ee Docs updates und restructuring (#14636) 2025-07-12 22:08:19 +02:00
Vasilis Spilka 9b729ca4c1 Add printable_limit and limit to IO.inspect doc examples (#14646) 2025-07-12 22:08:17 +02:00
Michał Łępicki 8cba20cb3e Clean up unreachable clause of Types.Descr.atom_only? helper (#14647)
it's being always called with a map
2025-07-12 22:08:10 +02:00
Jean Klingler b7e0d657d1 Drop :app_properties when rendering dependency in mix (#14645) 2025-07-12 21:11:21 +09:00
José Valim 2c3edbe1c7 Check for type equality 2025-07-11 16:15:53 +02:00
José Valim fec9899ead Revamp Mix & OTP guides (#14637) 2025-07-11 15:53:07 +02:00
Gary Rennie f54b192823 Add ETS to the Erlang Term Storage section of erlang libs (#14644)
This will ensure that it appears in the short search results on ExDoc
instead of having to navigate through to the search results.
2025-07-11 15:52:06 +02:00
Guillaume Duboc 855df4fc77 Domain keys in map (#14478)
- Introduced tests for union, intersection, and difference operations involving domain key types.
- Validated subtype relationships and intersection results for maps with domain keys.
- Enhanced map fetch and delete functionalities to handle domain key types.
- Ensured correct behavior of dynamic types with domain keys in various scenarios.
2025-07-11 15:52:06 +02:00
José Valim f87fbc2833 Clarify function types 2025-07-11 15:52:06 +02:00
José Valim 2b8ee38660 Tag / as an operator in fragments, closes #14643 2025-07-11 11:12:32 +02:00
José Valim 8d2775051d Update ... to an operator in Code.Fragment 2025-07-11 11:12:31 +02:00
Łukasz Samson 26855eda9f Update allow_local option spec (#14642) 2025-07-10 20:48:43 +02:00
José Valim ea77a68daa Add tests for allow_locals option 2025-07-10 17:32:08 +02:00
Łukasz Samson cb49bfb6e4 Add local_for_callback option to Macro.Env.expand_import (#14620) 2025-07-10 17:32:07 +02:00
Eksperimental c3ec400607 Use thin space (U+2009) as a separator instead of _ and in regular English language (#14635) 2025-07-10 10:22:11 +02:00
Eksperimental c03caa18cf Use backticks around literals in documentation (#14633) 2025-07-10 10:22:10 +02:00
José Valim bd68532c27 Remove explicit mentions to elixirc, as it isn't used in practice 2025-07-10 10:21:55 +02:00
Eksperimental a244323815 Correct grammar in structural sorting order section (#14639)
It is not required by the Elixir developer, but it is not required for them to know this by heart.

The former indicates that it is the Elixir developers who are not requiring this, the latter expresses
that they do not need to know this by heart.
2025-07-10 09:31:15 +02:00
Eksperimental fff2cf0d81 Standardize "Examples" heading section levels in docs (#14638)
* Convert "Examples" 3rd level headings to 2nd level when not under a 2nd level

* Convert 1st level "Examples" heading to 2nd level
2025-07-10 09:31:13 +02:00
José Valim b878f37577 Improve error message for protocols with no implementation, closes #14364 2025-07-10 09:30:42 +02:00
Jean Klingler 9619116cde Prevent mix test from overriding :failures_manifest_path option (#14632)
Introduced in 99be673
2025-07-08 16:08:16 +09:00
José Valim 248a71e2fc Fix logger docs
Closes #14628.
Closes #14629.
2025-07-06 09:46:35 +02:00
José Valim aa6964547d Fix return type of phi, closes #14621 2025-07-04 09:34:39 +02:00
José Valim 5c7687bf6f Apply further fn optimizations and fixes (#14619)
Closes #14598
2025-07-03 16:00:56 +02:00
Guillaume Duboc e73dd0c17d Perf optimizations and inferred intersections (#14605) 2025-07-03 16:00:48 +02:00
Guillaume Duboc f4037a31ba Simplified tuple definitions by removing negations (#14596) 2025-07-03 16:00:40 +02:00
José Valim a5b9c5b5db Add required field back to struct info
Closes #14616.
Closes #14617.
Closes #14500.
2025-07-03 12:22:25 +02:00
Łukasz Samson fcea4b4755 Handle filesystem errors in iex helpers (#14618)
`File.cd` and `File.ls` can return any posix error code
2025-07-03 10:29:41 +02:00
Michał Łępicki 24e63ccc47 Fix parallel option type in Mix.Compilers.Erlang.compile/6 spec (#14615)
compile.yecc and compile.leex tasks call it with parallel: true
2025-07-01 22:34:58 +02:00
Łukasz Samson d8bb1c4009 Add missing erlang compiler options (#14614)
Document options on leex and yecc compilers
2025-07-01 22:34:57 +02:00
José Valim fa2c96ffc3 Remove specs which are pass through and from private modules 2025-07-01 10:44:15 +02:00
Łukasz Samson db203167e2 Replace keyword with concrete keyword lists in specs (#14611) 2025-06-30 17:24:37 +02:00
José Valim a0ca37cb8f Fix warnings on Erlang/OTP 28 2025-06-30 11:57:26 +02:00
José Valim 592f44e2a8 Remove Regex warning until Erlang/OTP 28.1 2025-06-30 10:55:27 +02:00
José Valim 5ebe19e333 Make sure we log all output when partition fails 2025-06-27 15:30:41 +02:00
José Valim 9e3e817e42 Deal with undefined on :shell.whereis/0 2025-06-26 13:49:01 +02:00
José Valim a450d12912 Do not send quoted expressions to Macro.dbg 2025-06-26 13:38:17 +02:00
José Valim 65dbdef7dc Fix pry on Erlang/OTP 28 2025-06-26 13:32:57 +02:00
Jonatan Männchen 4f5746369b Use Workload Identity Federation for Windows Trusted Signing (#14604) 2025-06-25 19:31:00 +02:00
Steffen Deusch 274bc56f53 Add compilers option to Mix.install/2 (#14577) 2025-06-22 11:54:27 -07:00
José Valim 7d3047981c Distinguish source_anno from doc_anno, see #14595 2025-06-21 03:54:49 -07:00
José Valim 4f2868e632 Ensure block_keyword_or_binary_operator is handled in surround context, closes #14590 2025-06-21 03:25:44 -07:00
Łukasz Samson 41151190e5 Handle error result from unescape_tokens in tokenizer (#14587) 2025-06-19 12:02:26 -07:00
Łukasz Samson 711008a456 Consistently raise UnicodeConversionError in tokenizer (#14589) 2025-06-19 12:02:25 -07:00
Łukasz Samson 7a28203ff5 Fix invalid warning on no parens call on true (#14593) 2025-06-19 05:57:39 -07:00
José Valim f4bbf76d0b Optimize empty_difference_subtype? for dynamic parts 2025-06-14 12:13:19 -07:00
Jean Klingler 9fdb835f26 Mark inlined function call result as generated (#14581) 2025-06-14 19:07:36 +09:00
José Valim 95f982e004 Document bug fix on defstruct/defexception inside protocol, closes #14574 2025-06-12 11:36:47 +02:00
José Valim 7e08594ca0 Add tests for nested struct updates too 2025-06-11 17:05:24 +02:00
José Valim 2096f4156d Convert only the pattern matching suggestion into a hint 2025-06-11 14:08:07 +02:00
José Valim 9d845cf031 Update CHANGELOG 2025-06-11 13:59:26 +02:00
José Valim d74dff2bcf Transform the struct update syntax into a type assertion
This transforms the struct update into a type assertion,
requiring the type system to be sure the expression has
precisely the given struct type.

The struct update syntax may still be deprecated in the
future but this will provide a safer migration path and
allow us to engage in more conversations with the community.
2025-06-11 13:39:14 +02:00
José Valim 59a1ad9138 Allow captures to be reconstructed on type system pretty printing 2025-06-11 13:30:51 +02:00
José Valim 342c724863 Do no start listeners if --no-deps-check is given 2025-06-11 12:08:48 +02:00
José Valim 35818f1cdb Warn when invalid fun typespec is used 2025-06-11 10:25:24 +02:00
Joe Yates 502207c9f7 Fix use of prefer with '-ing' (#14568) 2025-06-10 11:17:13 +02:00
José Valim c3bc849550 Point out module must be required before macro usage in match/guard 2025-06-10 10:14:56 +02:00
José Valim f8de42a053 Filter @compile debug_info when explicitly set to true
Closes #14567.
2025-06-10 09:59:05 +02:00
José Valim ce33663780 Relax return type of impl_for with nil to avoid false positives 2025-06-10 09:49:55 +02:00
Tomasz Marek Sulima 752ca8864d Sort by call on tprof memory tests (Erlang/OTP 28) (#14565) 2025-06-09 22:27:08 +02:00
Theodor-Alexandru Irimia 8588a8d473 Clarify why and how to start second session for tests (#14566) 2025-06-09 22:27:08 +02:00
José Valim 2a9a4f2cab Release v1.19.0-rc.0 2025-06-09 12:27:46 +02:00
Michał Łępicki 0847e4b41a Fix mistake in "Untracked compile-time dependencies" anti-pattern (#14563) 2025-06-09 12:07:28 +02:00
José Valim 7aafd6ca77 Also download rebar3 automatically when compiling 2025-06-08 19:13:12 +02:00
José Valim 5ff421ca27 Disable inference as part of v1.19 release 2025-06-07 21:23:03 +02:00
José Valim 4425d684a0 Update CHANGELOG 2025-06-07 20:21:55 +02:00
José Valim e3f538cd25 Branch out v1.19 2025-06-07 20:18:21 +02:00
461 changed files with 14369 additions and 35288 deletions
+7 -12
View File
@@ -3,7 +3,8 @@
--- ---
name: Report an issue name: Report an issue
description: Tell us about something that is not working the way we (probably) intend description:
Tell us about something that is not working the way we (probably) intend
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -12,17 +13,11 @@ body:
Please, do not use this form for guidance, questions or support. Please, do not use this form for guidance, questions or support.
Try instead in [Elixir Forum](https://elixirforum.com) or any of Try instead in [Elixir Forum](https://elixirforum.com),
our online communities (Slack, Discord, etc). the [IRC Chat](https://web.libera.chat/#elixir),
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
- type: checkboxes [Slack](https://elixir-slackin.herokuapp.com),
id: existing-issue [Discord](https://discord.gg/elixir) or in other online communities.
attributes:
label: Existing issue
description: Please search [existing issues](https://github.com/elixir-lang/elixir/issues) before continuing.
options:
- label: I have searched existing issues and could not find a duplicate.
required: true
- type: textarea - type: textarea
id: elixir-and-otp-version id: elixir-and-otp-version
-2
View File
@@ -7,5 +7,3 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
cooldown:
default-days: 7
-4
View File
@@ -1,4 +0,0 @@
* Describe here the reasons behind the pull request.
* Make sure you have read the CONTRIBUTING.md file.
* Make sure any relevant documentation and tests have been added/updated.
* Do not submit Draft pull requests unless previously asked/agreed.
+40
View File
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: CI for Markdown content
on:
push:
branches:
- "main"
paths:
- "lib/**/*.md"
pull_request:
paths:
- "lib/**/*.md"
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
name: Lint Markdown content
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+83 -60
View File
@@ -1,13 +1,15 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team # SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: CI name: CI
on: on:
push: push:
paths-ignore:
- "lib/**/*.md"
pull_request: pull_request:
workflow_dispatch: paths-ignore:
- "lib/**/*.md"
env: env:
ELIXIR_ASSERT_TIMEOUT: 2000 ELIXIR_ASSERT_TIMEOUT: 2000
@@ -19,64 +21,65 @@ permissions:
jobs: jobs:
test_linux: test_linux:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }} name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- otp_version: "29.0" - otp_version: "28.1"
deterministic: true deterministic: true
- otp_version: "28.4" - otp_version: "28.1"
docs: true erlc_opts: "warnings_as_errors"
coverage: true coverage: true
- otp_version: "28.1" - otp_version: "28.1"
otp_latest: true
erlc_opts: "warnings_as_errors"
- otp_version: "27.3" - otp_version: "27.3"
erlc_opts: "warnings_as_errors"
- otp_version: "27.0" - otp_version: "27.0"
erlc_opts: "warnings_as_errors"
- otp_version: "26.0"
- otp_version: master - otp_version: master
development: true development: true
- otp_version: maint - otp_version: maint
development: true development: true
runs-on: ubuntu-24.04
# Earlier Erlang/OTP versions ignored compiler directives
# when using warnings_as_errors. So we only set ERLC_OPTS
# from Erlang/OTP 27+.
env: env:
ERLC_OPTS: "warnings_as_errors" ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with: with:
otp-version: ${{ matrix.otp_version }} otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS - name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }} if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir - name: Compile Elixir
run: | run: |
make compile make compile
echo "$PWD/bin" >> $GITHUB_PATH echo "$PWD/bin" >> $GITHUB_PATH
- name: Build info - name: Build info
run: bin/elixir --version run: bin/elixir --version
- name: Check format - name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted." run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite - name: Erlang test suite
run: make test_erlang run: make test_erlang
continue-on-error: ${{ matrix.development == true }} continue-on-error: ${{ matrix.development }}
- name: Elixir test suite - name: Elixir test suite
run: make test_elixir run: make test_elixir
continue-on-error: ${{ matrix.development == true }} continue-on-error: ${{ matrix.development }}
env: env:
COVER: "${{ matrix.coverage }}" COVER: "${{ matrix.coverage }}"
- name: "Calculate Coverage"
run: make cover | tee "$GITHUB_STEP_SUMMARY"
if: "${{ matrix.coverage }}"
- name: Build docs (ExDoc main) - name: Build docs (ExDoc main)
if: ${{ matrix.docs }} if: ${{ matrix.otp_latest }}
run: | run: |
cd .. cd ..
git clone https://github.com/elixir-lang/ex_doc.git --depth 1 git clone https://github.com/elixir-lang/ex_doc.git --depth 1
@@ -85,67 +88,87 @@ jobs:
cd ../elixir/ cd ../elixir/
git fetch --tags git fetch --tags
DOCS_OPTIONS="--warnings-as-errors" make docs DOCS_OPTIONS="--warnings-as-errors" make docs
- name: Check reproducible builds
- name: "Calculate Coverage" if: ${{ matrix.deterministic }}
if: ${{ matrix.coverage }} run: |
run: make cover | tee "$GITHUB_STEP_SUMMARY" rm -rf .git
# Recompile System without .git
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
- name: "Upload Coverage Artifact" - name: "Upload Coverage Artifact"
if: ${{ matrix.coverage }} if: "${{ matrix.coverage }}"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: TestCoverage name: TestCoverage
path: cover/* path: cover/*
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: taskset 1 make check_reproducible
- name: Check git is not required
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
cd lib/elixir
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
test_windows: test_windows:
name: Windows Server 2022, OTP ${{ matrix.otp_version }} name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
runs-on: windows-2022
strategy: strategy:
matrix: matrix:
otp_version: otp_version: ["26.2", "27.3", "28.1"]
- "29.0" runs-on: windows-2022
- "28.1"
- "27.3"
steps: steps:
- name: Configure Git - name: Configure Git
run: git config --global core.autocrlf input run: git config --global core.autocrlf input
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with: with:
otp-version: ${{ matrix.otp_version }} otp-version: ${{ matrix.otp_version }}
- name: Compile Elixir - name: Compile Elixir
run: | run: |
Remove-Item -Recurse -Force '.git' Remove-Item -Recurse -Force '.git'
make compile make compile
- name: Build info - name: Build info
run: bin/elixir --version run: bin/elixir --version
- name: Check format - name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted." run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite - name: Erlang test suite
run: make test_erlang run: make test_erlang
- name: Elixir test suite - name: Elixir test suite
run: | run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts' Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
make test_elixir make test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- name: Install Shellcheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Check POSIX-compliant
run: |
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
license_compliance:
name: Check Licence Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Run OSS Review Toolkit"
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
-55
View File
@@ -1,55 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 The Elixir Team
name: "CodeQL Advanced"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "29 8 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: "ubuntu-latest"
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"
zizmor:
name: Zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
-41
View File
@@ -1,41 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: License Compliance
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
license_compliance:
name: Check License Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run OSS Review Toolkit
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
-41
View File
@@ -1,41 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Markdown Content
on:
push:
branches:
- "main"
paths: &paths-filter
- "**/*.md"
- .github/workflows/markdown.yml
- .markdownlint-cli2.jsonc
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
lint:
name: Lint Markdown content
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
+1 -1
View File
@@ -74,6 +74,6 @@ unless System.get_env("DRYRUN") do
"api-username" => "Elixir" "api-username" => "Elixir"
} }
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers) resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}") IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
end end
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team # SPDX-FileCopyrightText: 2021 The Elixir Team
name: Release Notifications name: Notify
on: on:
release: release:
@@ -15,20 +15,17 @@ jobs:
notify: notify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Notify name: Notify
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with: with:
otp-version: "27.3" otp-version: "27.3"
elixir-version: "1.18.3" elixir-version: "1.18.3"
- name: Run Elixir script - name: Run Elixir script
env: env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }} ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }} ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: | run: |
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME" elixir .github/workflows/notify.exs ${{ github.ref_name }}
+2 -5
View File
@@ -46,7 +46,6 @@ runs:
repository: oss-review-toolkit/ort-config repository: oss-review-toolkit/ort-config
ref: "main" ref: "main"
path: ".ort-config" path: ".ort-config"
persist-credentials: false
- name: Setup ORT Config - name: Setup ORT Config
id: setup-ort-config id: setup-ort-config
@@ -89,9 +88,9 @@ runs:
- name: Run OSS Review Toolkit - name: Run OSS Review Toolkit
id: ort id: ort
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0 uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with: with:
image: ghcr.io/oss-review-toolkit/ort:92.2.0 image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >- run: >-
labels, labels,
cache-dependencies, cache-dependencies,
@@ -107,6 +106,4 @@ runs:
ort-cli-report-args: >- ort-cli-report-args: >-
-O CycloneDX=output.file.formats=json,xml -O CycloneDX=output.file.formats=json,xml
-O SpdxDocument=outputFileFormats=JSON,YAML -O SpdxDocument=outputFileFormats=JSON,YAML
ort-cli-scan-args: >-
--scanners Provenant
sw-version: "${{ inputs.version }}" sw-version: "${{ inputs.version }}"
-53
View File
@@ -1,53 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: POSIX Compliance
on:
push:
paths: &paths-filter
- .github/workflows/posix_compliance.yml
- bin/elixir
- bin/elixirc
- bin/iex
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
check_posix_compliance:
name: Check POSIX compliance
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install ShellCheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Run ShellCheck on bin/ dir
run: |
shellcheck -e SC2039,2086 bin/elixir && \
echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && \
echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && \
echo "bin/iex is POSIX compliant"
+61 -77
View File
@@ -1,19 +1,16 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team # SPDX-FileCopyrightText: 2021 The Elixir Team
name: Releases name: Release
on: on:
push: push:
branches: branches:
- main - main
- v*.* - v*.*
tags: tags:
- v* - v*
workflow_dispatch:
env: env:
ELIXIR_OPTS: "--warnings-as-errors" ELIXIR_OPTS: "--warnings-as-errors"
LANG: C.UTF-8 LANG: C.UTF-8
@@ -23,68 +20,64 @@ permissions:
jobs: jobs:
create_draft_release: create_draft_release:
name: Create draft release runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions: permissions:
contents: write contents: write
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Create draft release - name: Create draft release
if: github.ref_type != 'branch' if: github.ref_type != 'branch'
run: | run: |
gh release create \ gh release create \
--repo "$GITHUB_REPOSITORY" \ --repo ${{ github.repository }} \
--title "$GITHUB_REF_NAME" \ --title ${{ github.ref_name }} \
--notes '' \ --notes '' \
--draft \ --draft \
"$GITHUB_REF_NAME" ${{ github.ref_name }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# zizmor: ignore[artipacked]
if: github.ref_type == 'branch' if: github.ref_type == 'branch'
with:
fetch-depth: 50
- name: Update ${{ github.ref_name }}-latest - name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch' if: github.ref_type == 'branch'
run: | run: |
ref_name="${GITHUB_REF_NAME}-latest" ref_name=${{ github.ref_name }}-latest
if ! gh release view "$ref_name"; then if ! gh release view $ref_name; then
gh release create \ gh release create \
--latest=false \ --latest=false \
--title "$ref_name" \ --title $ref_name \
--notes "Automated release for latest ${GITHUB_REF_NAME}." \ --notes "Automated release for latest ${{ github.ref_name }}." \
"$ref_name" $ref_name
fi fi
git tag "$ref_name" --force git tag $ref_name --force
git push origin "$ref_name" --force git push origin $ref_name --force
build: build:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }} name: "Build Elixir"
runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
include: include:
- otp: 26
otp_version: "26.0"
- otp: 27 - otp: 27
otp_version: "27.0" otp_version: "27.0"
- otp: 28 - otp: 28
otp_version: "28.0" otp_version: "28.0"
build_docs: build_docs build_docs: build_docs
- otp: 29 runs-on: ubuntu-22.04
otp_version: "29.0"
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false fetch-depth: 50
- name: "Build Release" - name: "Build Release"
uses: ./.github/workflows/release_pre_built uses: ./.github/workflows/release_pre_built
@@ -99,39 +92,38 @@ jobs:
shasum -a 1 Docs.zip > Docs.zip.sha1sum shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload Linux release artifacts" - name: "Upload linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: build-linux-elixir-otp-${{ matrix.otp }} name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload Windows release artifacts" - name: "Upload windows release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: build-windows-elixir-otp-${{ matrix.otp }} name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts" - name: "Upload doc artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: matrix.build_docs if: matrix.build_docs
with: with:
name: Docs name: Docs
path: Docs.zip* path: Docs.zip*
sign: sign:
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
needs: [build] needs: [build]
environment: release environment: release
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
otp: [27, 28, 29] otp: [26, 27, 28]
flavor: [windows, linux] flavor: [windows, linux]
env: env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }} RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }} runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
permissions: permissions:
contents: write contents: write
@@ -139,20 +131,19 @@ jobs:
steps: steps:
- name: "Download build" - name: "Download build"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }} name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure - name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with: with:
client-id: ${{ secrets.AZURE_CLIENT_ID }} client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing" - name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with: with:
endpoint: https://eus.codesigning.azure.net/ endpoint: https://eus.codesigning.azure.net/
@@ -181,15 +172,17 @@ jobs:
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum" shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum" shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload Linux release artifacts" - name: "Upload linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }} name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}* path: ${{ env.RELEASE_FILE }}*
sbom: sbom:
name: Generate SBoM name: Generate SBoM
needs: [build, sign] needs: [build, sign]
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions: permissions:
@@ -205,13 +198,11 @@ jobs:
- name: Checkout project - name: Checkout project
id: checkout id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Download Build Artifacts" - name: "Download Build Artifacts"
id: download-build-artifacts id: download-build-artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
pattern: "{sign-*-elixir-otp-*,Docs}" pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true merge-multiple: true
@@ -226,7 +217,7 @@ jobs:
- name: Attest Distribution Assets with SBoM - name: Attest Distribution Assets with SBoM
id: attest-sbom id: attest-sbom
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
with: with:
subject-path: | subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip} /tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -246,19 +237,15 @@ jobs:
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore" cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done done
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore" cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore" cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore" cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore" cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
env: env:
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}" ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Assemble Release SBoM Artifacts" - name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: "SBoM" name: "SBoM"
path: | path: |
@@ -268,38 +255,37 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }} ${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations" - name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: "Attestations" name: "Attestations"
path: "attestations/*.sigstore" path: "attestations/*.sigstore"
upload-release: upload-release:
name: Upload release
needs: [create_draft_release, build, sign, sbom] needs: [create_draft_release, build, sign, sbom]
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}" pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true merge-multiple: true
- name: Upload Pre-build - name: Upload Pre-built
shell: bash shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
if [ "$GITHUB_REF_TYPE" == "branch" ]; then if [ "${{ github.ref_type }}" == "branch" ]; then
tag="${GITHUB_REF_NAME}-latest" tag=${{ github.ref_name }}-latest
else else
tag="$GITHUB_REF_NAME" tag="${{ github.ref_name }}"
fi fi
gh release upload \ gh release upload \
--repo "$GITHUB_REPOSITORY" \ --repo ${{ github.repository }} \
--clobber \ --clobber \
"$tag" \ "$tag" \
elixir-otp-*.zip \ elixir-otp-*.zip \
@@ -314,18 +300,15 @@ jobs:
bom.* bom.*
upload-builds-hex-pm: upload-builds-hex-pm:
name: Upload builds to hex.pm
runs-on: ubuntu-24.04
needs: [build, sign] needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm concurrency: builds-hex-pm
environment: release environment: release
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.HEX_AWS_REGION }} AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }} AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps: steps:
- name: "Check if variables are set up" - name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}" if: "${{ ! vars.HEX_AWS_REGION }}"
@@ -333,7 +316,7 @@ jobs:
echo "Required variables for uploading to hex.pm are not set up, skipping..." echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1 exit 1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
pattern: "{sign-*-elixir-otp-*,Docs}" pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true merge-multiple: true
@@ -344,10 +327,12 @@ jobs:
- name: Upload Precompiled to S3 - name: Upload Precompiled to S3
run: | run: |
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1) oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${GITHUB_REF_NAME}} dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/} surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \ aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
@@ -356,16 +341,16 @@ jobs:
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \ aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
--cache-control "public,max-age=3600" \ --cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt echo builds/elixir/${ref_name} >> purge_keys.txt
fi fi
done done
- name: Upload Docs to S3 - name: Upload Docs to S3
run: | run: |
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g') version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
unzip Docs.zip unzip Docs.zip
@@ -386,7 +371,7 @@ jobs:
- name: Update builds txt - name: Update builds txt
run: | run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name="$GITHUB_REF_NAME" ref_name=${{ github.ref_name }}
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1) oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
@@ -439,7 +424,6 @@ jobs:
for key in $(cat purge_keys.txt); do for key in $(cat purge_keys.txt); do
purge "${key}" purge "${key}"
done done
env: env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }} FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }} FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
+10 -24
View File
@@ -1,58 +1,45 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team # SPDX-FileCopyrightText: 2021 The Elixir Team
name: Release Pre-build name: "Release pre built"
description: "Builds Elixir release, ExDoc and generates docs" description: "Builds elixir release, ExDoc and generates docs"
inputs: inputs:
otp: otp:
description: "The major OTP version" description: "The major OTP version"
otp_version: otp_version:
description: "The exact OTP version (major.minor[.patch])" description: "The exact OTP version (major.minor[.patch])"
build_docs: build_docs:
description: "Whether docs have to be built" description: "If docs have to be built or not"
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 - uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
with: with:
otp-version: ${{ inputs.otp_version }} otp-version: ${{ inputs.otp_version }}
version-type: strict version-type: strict
- name: Build Elixir Release - name: Build Elixir Release
shell: bash shell: bash
run: | # zizmor: ignore[github-env] run: |
make Precompiled.zip make Precompiled.zip
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip" mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
echo "$PWD/bin" >> $GITHUB_PATH echo "$PWD/bin" >> $GITHUB_PATH
env:
INPUT_OTP: ${{ inputs.otp }}
- name: Install NSIS - name: Install NSIS
shell: bash shell: bash
run: | run: |
sudo apt update sudo apt update
sudo apt install -y nsis sudo apt install -y nsis
- name: Build Elixir Windows Installer - name: Build Elixir Windows Installer
shell: bash shell: bash
run: | run: |
export OTP_VERSION="$INPUT_OTP_VERSION" export OTP_VERSION=${{ inputs.otp_version }}
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip" export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh) (cd lib/elixir/scripts/windows_installer && ./build.sh)
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" . mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
env:
INPUT_OTP: ${{ inputs.otp }}
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
- name: Get ExDoc ref - name: Get ExDoc ref
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
shell: bash shell: bash
run: | # zizmor: ignore[github-env] run: |
if [ "$GITHUB_REF_NAME" = "main" ]; then if [ "${{ github.ref_name }}" = "main" ]; then
ref=main ref=main
else else
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version') ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
@@ -64,7 +51,6 @@ runs:
repository: elixir-lang/ex_doc repository: elixir-lang/ex_doc
ref: ${{ env.EX_DOC_REF }} ref: ${{ env.EX_DOC_REF }}
path: ex_doc path: ex_doc
persist-credentials: false
- name: Build ex_doc - name: Build ex_doc
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
shell: bash shell: bash
+2 -3
View File
@@ -10,10 +10,9 @@
/lib/elixir/test/ebin/ /lib/elixir/test/ebin/
/man/elixir.1 /man/elixir.1
/man/iex.1 /man/iex.1
/Docs.zip /Docs-v*.zip
/Precompiled.zip /Precompiled-v*.zip
/.eunit /.eunit
.elixir.plt .elixir.plt
erl_crash.dump erl_crash.dump
/cover/ /cover/
.tool-versions
-63
View File
@@ -1,63 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
"globs": [
"**/*.md"
],
"ignores": [
".git/**",
".github/**"
],
"gitignore": true,
"config": {
// Consecutive header levels (h1 -> h2 -> h3).
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content.
"MD024": {
// Duplication is allowed for headings with different parents.
"siblings_only": true
},
// Trailing punctuation in heading.
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
// the default values.
"MD026": {
"punctuation": ".,;:。,;:!"
},
// 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"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false,
// Our tables are too large to align.
"MD060": false
}
}
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"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"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false
}
-5
View File
@@ -37,11 +37,6 @@ curations:
comment: "Apply Trademark Policy to VERSION file" comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE" detected_license: "NONE"
concluded_license: "Apache-2.0" concluded_license: "Apache-2.0"
- path: ".github/pull_request_template.md"
reason: "NOT_DETECTED"
comment: "Apply default license to GitHub pull request template"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified # Wrongly Identified
- path: ".gitignore" - path: ".gitignore"
-1
View File
@@ -1 +0,0 @@
CONTRIBUTING.md
+481 -78
View File
@@ -4,108 +4,511 @@
SPDX-FileCopyrightText: 2012 Plataformatec SPDX-FileCopyrightText: 2012 Plataformatec
--> -->
# Changelog for Elixir v1.21 # Changelog for Elixir v1.19
## v1.21.0-dev ## Type system improvements
### Type checking of protocol dispatch and implementations
This release also adds type checking when dispatching and implementing protocols.
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
```elixir
defmodule Example do
def my_code(first..last//step = range) do
"hello #{range}"
end
end
```
the above emits the following warnings:
```
warning: incompatible value given to string interpolation:
data
it has type:
%Range{first: term(), last: term(), step: term()}
but expected a type that implements the String.Chars protocol, it must be one of:
dynamic(
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
%Version.Requirement{}
) or atom() or binary() or float() or integer() or list(term())
```
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
```elixir
defmodule Example do
def my_code(%Date{} = date) do
for(x <- date, do: x)
end
end
```
will emit:
```
warning: incompatible value given to for-comprehension:
x <- date
it has type:
%Date{year: term(), month: term(), day: term(), calendar: term()}
but expected a type that implements the Enumerable protocol, it must be one of:
dynamic(
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
) or fun() or list(term()) or non_struct_map()
```
### Type checking and inference of anonymous functions
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
```elixir
defmodule Example do
def run do
fun = fn %{} -> :map end
fun.("hello")
end
end
```
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
```
warning: incompatible types given on function application:
fun.("hello")
given types:
binary()
but function has type:
(dynamic(map()) -> :map)
typing violation found at:
│
6 │ fun.("hello")
│ ~
│
└─ mod.exs:6:8: Example.run/0
```
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
### Acknowledgements
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/).
## Faster compile times in large projects
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU cores.
### Code loading bottlenecks
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing parallelization, especially on large projects.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server and the amount of work during compilation, with reports of more than two times faster compilation for large projects. The benefits depend on the codebase size and the number of CPU cores available.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
There are two potential regressions with this approach. The first one happens if you spawn processes during compilation which invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
list = [...]
Task.async_stream(list, fn item ->
MyLib.SomeOtherModule.do_something(item)
end)
end
```
Because the spawned process is not visible to the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_compiled!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
The second one is related to `@on_load` callbacks (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
@on_load :init
def init do
MyLib.AnotherModule.do_something()
end
def something_else do
...
end
end
MyLib.SomeModule.something_else()
```
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
Both snippets above could actually lead to non-deterministic compilation failures in the past, and as a result of these changes, compiling these cases are now deterministic.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, as outlined in the previous section, there were pathological cases where performance gains would be left on the table, such as when compiling dependencies with native code or dependencies where one or two large files would take most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
## Improved pretty printing algorithm
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
```elixir
[
[1, 2, 3],
[1, 2, ...],
[1, ...],
[...],
...
]
```
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
```elixir
[
[1, 2, 3],
...
]
```
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
## Erlang/OTP 28 support
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped into abstract syntax trees by defining a `__escape__/1` callback.
On the other hand, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
```elixir
defmodule Foo do
defstruct regex: ~r/foo/
end
```
You must do this:
```elixir
defmodule Foo do
defstruct [:regex]
def new do
%Foo{regex: ~r/foo/}
end
end
```
## OpenChain certification
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
* Each release is attested along with the Source SBoM.
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.6 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
## v1.19.5 (2026-01-09)
### 1. Enhancements ### 1. Enhancements
#### EEx
* [EEx] Support splitting middle expressions across EEx clauses
#### Elixir #### Elixir
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1` * [Protocol] Optimize protocol consolidation to no longer load structs
* [Code] Add support for the `:erlc_options` compiler option
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
* [Kernel] Improve performance of type constructors and complex intersections
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
* [Path] Add `Path.safe_join/2`
* [Registry] Optimize exact key matching in lookups
* [String] Optimize `String.bag_distance/2`
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
* [URI] Optimize percent-decoding and `URI.to_string/1`
#### ExUnit
* [ExUnit.Assertions] Add `trace/3` helper
### 2. Bug fixes ### 2. Bug fixes
#### Elixir #### Elixir
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y` * [Kernel] Fix unnecessary recompilation when `dbg_callback` is modified at runtime
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3` * [Kernel] Fix parser crash on missing parentheses on expression following operator `not in`
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision * [Kernel] Support fetching abstract code for modules compiled with Elixir v1.14 and earlier
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats * [Protocol] Ensure protocol consolidation no longer stores outdated struct types. As a consequence, protocols types only track struct names at the moment
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments * [Stream] Revert optimization which caused nested streams in `Stream.flat_map/2` to crash
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
* [Date.Range] Fix slicing date ranges with stepped ranges
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
* [Kernel] Fix inferred types for functions with non-returning clauses
* [Kernel] Fix map field type inference in the presence of empty map types
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
* [Kernel] Improve the error message for non-atom struct keys
* [Kernel] Raise when `|` is used in guards
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
* [Macro] Properly escape C1 control characters and Unicode noncharacters
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
#### ExUnit
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
#### IEx #### IEx
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys * [IEx] Fix usage of `#iex:break` as part of multi-line prompts
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file #### Logger
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
* [Logger.Backends] Do not crash on invalid metadata
## v1.19.4 (2025-11-27)
### 1. Enhancements
#### Mix #### Mix
* [Mix] Prevent synchronization lock files from being overwritten with empty contents * [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
* [mix new] Avoid trailing whitespace in generated files
### 3. Hard deprecations ### 2. Bug fixes
#### Elixir #### Elixir
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead * [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
### 4. Soft deprecations #### ExUnit
* [ExUnit] Fix formatter crash when diffing takes too long
* [ExUnit] Ensure parallel matches in `assert` propagate type information
#### Logger
* [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters)
#### Mix
* [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled
* [mix escript.build] Ensure the `hex` application can be included in escripts
## v1.19.3 (2025-11-13)
### 1. Enhancements
#### Elixir #### Elixir
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1` * [Kernel] Support /E modifier for regular expressions in config files
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
## v1.20 #### Mix
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md). * [mix compile] Allow forcing specific compilers, such as `--force-elixir`, `--force-app`, etc
* [mix help app:APP] Support showing helps for apps in Elixir and Erlang standard libraries
### 2. Bug fixes
#### Elixir
* [IO] Fix dialyzer warning on `IO.inspect :label`
#### ExUnit
* [ExUnit.Case] Fix crash when formatting errors caused by a linked/trapped exit during `setup_all`
#### Mix
* [mix compile.app] Ensure functions in the format `&Mod.fun/arity` can be written to .app files
* [mix compile.app] Ensure strings with Unicode characters can be written to .app files
## v1.19.2 (2025-11-02)
### 1. Enhancements
#### Elixir
* [Kernel] Measure and optimize writing of `.beam` files in the compiler
* [Kernel] Optimize rare scenarios where type checking took too long
#### Mix
* [mix compile] Add flag `--no-check-cwd` to skip compiler check to aid debugging
### 2. Bug fixes
#### Elixir
* [IO] Fix dialyzer warning on `IO.inspect :label`
* [Kernel] Ensure we warn on deprecated `~~~` unary operator
#### Logger
* [Logger] Reset ansi escapes before newlines in Logger
#### Mix
* [mix compile] Warn if `elixirc_paths` is not a list of string paths
* [mix compile] Address regression where umbrella children were compiled too early and without respecting compilation flags
* [mix deps.compile] Improve reliability of `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` across `mix escript.install`, `mix archive.install`, and others
## v1.19.1 (2025-10-20)
### 1. Bug fixes
#### EEx
* [EEx] Address Dialyzer warnings when invoking `EEx.compile_string`
#### Elixir
* [Kernel] Optimize how types are computed for pretty printing
* [Kernel] Optimize how differences are computed in the type system
* [Macro] Do not escape options given to `dbg/2`
* [Protocol] Improve protocol violation warnings
#### Mix
* [mix compile] Do not attempt to touch deleted files when compilation fails and then resumed with missing files
* [mix deps.compile] Do not spawn partitions when all dependencies are local and already compiled
## v1.19.0 (2025-10-16)
### 1. Enhancements
#### Elixir
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
* [Code.Fragment] Add `Code.Fragment.lines/1`
* [Enum] Provide more information on `Enum.OutOfBoundsError`
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
* [Registry] Add key-based partitioning of duplicate registries
* [String] Add `String.count/2` to count occurrences of a pattern
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
#### IEx
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun`, `mix help Mod.fun/arity`, and `mix help app:package`
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [Mix] Support the `:compilers` option
* [Mix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
#### ExUnit
* [ExUnit.Assertions] Fix order of pinned variables in failure reports
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
* [ExUnit.Case] Do not crash on empty test unit groups
#### IEx
* [IEx] Abort pipelines when there is an error in any step along the way
#### Mix
* [mix cmd] Preserve argument quoting in subcommands by no longer performing shell expansion. To revert to the previous behaviour, pass `--shell` before the command name
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
* [mix xref graph] Provide more consistent output by considering strong connected components when computing graphs. This means that, if you were using `mix xref graph --format cycles`, you will now get fewer but larger cycles, as cycle between `A` and `B` and another between `A` and `C` is now considered a single cycle between `A`, `B`, and `C`. Note the cycles themselves are not problematic, unless they have compile-time dependencies in them
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and the task itself has no effect
### 4. Hard deprecations
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}`, now requires the given variable (or expression) to explicitly pattern match on the struct before it can be updated. This is because, thanks to the type system, pattern matching on structs can find more errors, more reliably, and we want to promote its usage. Once pattern matching is added, you may optionally convert the struct update syntax into the map update syntax `%{uri | path: "/foo/bar"}` with no less of typing guarantees
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
#### Logger
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
#### Mix
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
## v1.18
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
+5 -5
View File
@@ -6,7 +6,7 @@
# Code of Conduct # Code of Conduct
Contact: <elixir-lang-conduct@googlegroups.com> Contact: elixir-lang-conduct@googlegroups.com
## Why have a Code of Conduct? ## Why have a Code of Conduct?
@@ -51,15 +51,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project: Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
* The [official GitHub projects][1] and code reviews. * The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists. * The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3]. * The **[#elixir][2]** IRC channel on [Libera.Chat][3].
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information. Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group. **The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
+67 -114
View File
@@ -55,7 +55,7 @@ To recompile all (including Erlang modules):
make compile make compile
``` ```
After your changes are done, run `make format` to guarantee 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, then run the full suite with
`make test`. `make test`.
@@ -71,101 +71,80 @@ 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). [the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile). More tasks can be found by reading the [Makefile](Makefile).
## Sending a pull request We encourage contributors to write tests that capture both existing and newly
introduced behavior, especially for bug fixes and major changes:
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
and must include tests and other relevant proof of work:
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before
your change and *passes* afterward. This makes it easier to confirm that the
fix addresses the underlying issue and helps prevent regressions in the future.
* **Bug Fixes:** If you are fixing a bug, please try to include a test that
*fails* before your change and *passes* afterward. This makes it easier to
confirm that the fix addresses the underlying issue and helps prevent
regressions in the future.
* **New Features or Major Changes:** If you are adding a new feature or making * **New Features or Major Changes:** If you are adding a new feature or making
major changes to existing functionality, please add assocaited tests. Aim to major changes to existing functionality, please add tests that cover the
have the best code coverage possible. major parts of that functionality. Aim to have the best code coverage possible.
* **Performance improvements:** For performance improvements, please include the With tests running and passing, you are ready to contribute to Elixir and
benchmark script, with inputs and results, in the pull request description. [send a pull request](https://help.github.com/articles/using-pull-requests/).
We recommend using [benchee](https://github.com/bencheeorg/benchee). We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
Here are some pull requests we have received in the past you can use as reference: * [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 Enum.member?](https://github.com/elixir-lang/elixir/pull/992) * [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes ## Reviewing changes
Once a pull request is sent, the Elixir team will review your changes. Once a pull request is sent, the Elixir team will review your changes.
If changes are necessary, the team will leave appropriate comments requesting We outline our process below to clarify the roles of everyone involved.
changes to the code. Unfortunately, we cannot guarantee a pull request will
be merged, even when modifications are requested, as the Elixir team will All pull requests must be approved by two committers before being merged into
re-evaluate the contribution as it changes. the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
When the review finishes, your pull request will be squashed and merged When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in believe they should be merged without squashing, please mention it in
a comment. a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is cloned and compiled alongside Elixir.
After cloning and compiling Elixir, run:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
## Licensing and Compliance Requirements ## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
guidelines on licensing and compliance. Below is a summary of the key points guidelines on licensing and compliance. Below is a summary of the key points
affecting **all external contributors**: affecting **all external contributors**:
* Accepted Licenses: Any code contributed must be licensed under the - Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license. `Apache-2.0` license.
- SPDX License Headers: With the exception of approved test fixture files,
* SPDX License Headers: With the exception of approved test fixture files, all new or modified files in a pull request must include correct SPDX
all new or modified files in a pull request must include correct SPDX headers. If you are creating a new file under the `Apache-2.0` license, for
headers. If you are creating a new file under the `Apache-2.0` license, for instance, please use:
instance, please use:
```elixir ```elixir
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team # SPDX-FileCopyrightText: 2021 The Elixir Team
``` ```
- No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
- Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
- Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
- Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
* No Executable Binaries: Contributions must **not** include any executable ```
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
* Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
* Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
* Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```text
By making a contribution to this project, I certify that: By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I (a) The contribution was created in whole or in part by me and I
@@ -192,53 +171,27 @@ affecting **all external contributors**:
involved. involved.
``` ```
See <https://developercertificate.org/> for a copy of the Developer Certificate See http://developercertificate.org/ for a copy of the Developer Certificate
of Origin license. of Origin license.
## Using AI and coding agents ## Building documentation
While we allow the use of AI on contributions and discussions, please be mindful Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
when doing so. Generally speaking, Elixir maintainers already have access to AI is installed and built alongside Elixir:
(like many other developers). Therefore, if we need the feedback or help of a
coding agent, we can request so ourselves. For this reason, we often find
the point of view of the human behind the agent more valuable.
That said, here are examples of how one might (or might not) use AI and coding ```sh
agents in Elixir spaces: # After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
```
* When it comes to discussions, using AI to help express yourself is welcome, Now go back to Elixir's root directory and run:
but avoid directly copy and pasting AI generated content. If there is a language
barrier, use AI to translate, review, and improve your text, but do not use AI
to respond on your behalf.
* Do not use coding agents to tackle existing issues unless they have the ```sh
"Contributions Welcome" label. make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
```
* If you request a feature on the mailing list and it is accepted, you may This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
use coding agents to implement it, as long as it follows the AI Contributions and `mix` under the `doc` directory. If you are planning to contribute documentation,
guidelines below. [please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
* When automating AI usage on the Elixir codebase for performance improvements,
security fixes, or correctness changes to the compiler or type system, pair it
with a separate set of agents whose job is to argue against and try to invalidate
any proposed change. And treat their approval as advisory: a human must still
validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below.
### AI contributions
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
Certificate of Origin (DCO). The human submitter is responsible for:
* Reviewing all AI-generated code
* Ensuring compliance with licensing requirements
* Adding their own Signed-off-by tag to certify the DCO
* Taking full responsibility for the contribution
* Disclosing use of AI for comments and code contributions
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
AI in the development process. Contributions should include an Assisted-by tag in the
following format:
Assisted-by: AGENT_NAME:MODEL_VERSION
+5 -7
View File
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs" TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/ # CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []' ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include ERLC := erlc -I lib/elixir/include
@@ -32,9 +32,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
#==> Functions #==> Functions
define CHECK_ERLANG_RELEASE define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 27)])' -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 >= 26)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \ if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \ echo "At least Erlang/OTP 26.0 is required to build Elixir"; \
exit 1; \ exit 1; \
fi fi
endef endef
@@ -107,10 +107,8 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
fi fi
@ echo "==> elixir (compile)"; @ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin; $(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
$(Q) $(GENERATE_APP) $(VERSION)
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP) $(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(Q) $(GENERATE_APP) $(VERSION) $(Q) $(GENERATE_APP) $(VERSION)
unicode: $(UNICODE) unicode: $(UNICODE)
@@ -189,7 +187,7 @@ clean_elixir:
#==> Documentation tasks #==> Documentation tasks
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}") 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://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(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)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)" 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 docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
+13 -10
View File
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
## 2. Scope ## 2. Scope
This policy applies to the Elixir Programming language, located at This policy applies to the Elixir Programming language, located at
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution https://github.com/elixir-lang/elixir. It covers every file, and contribution
made, including documentation and any associated assets. made, including documentation and any associated assets.
## 3. Licensing ## 3. Licensing
@@ -29,19 +29,18 @@ All code released by the Elixir team is licensed under the
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following [Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
licenses are recognized as permissible in this project: licenses are recognized as permissible in this project:
- The Unicode license, as documented at - The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt) [LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
- The Elixir Trademark Policy, as documented at [LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
These licenses are considered acceptable for any files or code that form part of These licenses are considered acceptable for any files or code that form part of
an Elixir repository. If a contribution requires a different license, it must an Elixir repository. If a contribution requires a different license, it must
either be rejected or prompt an update to this policy. either be rejected or prompt an update to this policy.
## 4. Contributing to the Elixir repository ## 4. Contributing to Elixir Projects
Any code contributed to the Elixir repository must fall under one of the accepted Any code contributed to Elixir repositories must fall under one of the accepted
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
other license will be rejected unless this policy is formally revised to include other license will be rejected unless this policy is formally revised to include
that license. All files except those specifically exempted (e.g., certain test that license. All files except those specifically exempted (e.g., certain test
@@ -52,9 +51,13 @@ configuration and undergo review.
Contributions must not introduce executable binary files into the codebase. Contributions must not introduce executable binary files into the codebase.
Every Elixir project within the organization will have an automated GitHub
Action to enforce these rules. This mechanism aids in detecting non-compliant
licenses or files early in the review process.
## 5. Preservation of Copyright and License Information ## 5. Preservation of Copyright and License Information
Any third-party code incorporated into the Elixir repository must retain original Any third-party code incorporated into Elixir projects must retain original
copyright and license headers. If no such headers exist in the source, they must copyright and license headers. If no such headers exist in the source, they must
be added. This practice ensures that original authors receive proper credit and be added. This practice ensures that original authors receive proper credit and
that the licensing lineage is preserved. that the licensing lineage is preserved.
@@ -162,4 +165,4 @@ necessary, by the EEF CISO. Any significant changes will be communicated to
contributors and made publicly available. contributors and made publicly available.
*Effective Date: 2025-02-20* *Effective Date: 2025-02-20*
*Last Reviewed: 2025-11-20* *Last Reviewed: 2025-02-20*
+6 -7
View File
@@ -7,14 +7,14 @@
<h1> <h1>
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png"> <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/public/images/logo/logo.png" width="200"> <img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture> </picture>
</h1> </h1>
[![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/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187)
Elixir is a programming language designed for building scalable Elixir is a dynamic, functional language designed for building scalable
and maintainable applications. and maintainable applications.
For more about Elixir, installation and documentation, For more about Elixir, installation and documentation,
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
comment and we can always reopen the issue. comment and we can always reopen the issue.
By keeping the overall issues tracker tidy and organized, the community By keeping the overall issues tracker tidy and organized, the community
can easily peek at what is coming in new releases and also get involved can easily peak at what is coming in new releases and also get involved
by commenting on existing issues and submitting pull requests. Please by commenting on existing issues and submitting pull requests. Please
remember to keep the tone positive and be kind! For more information, remember to keep the tone positive and be kind! For more information,
see the [Code of Conduct][1]. see the [Code of Conduct][1].
@@ -122,10 +122,9 @@ variable `ERL_COMPILER_OPTIONS=deterministic`.
Contributions to Elixir are always welcome! Before you get started, please check Contributions to Elixir are always welcome! Before you get started, please check
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
guidelines on how to set up your environment, run the test suite, format your guidelines on how to set up your environment, run the test suite, format your
code, and submit pull requests. code, and submit pull requests. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
Note you must disclose the use of coding agents and AI written code in your contribution experience.
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
## Development links ## Development links
+2 -2
View File
@@ -24,7 +24,7 @@
8. 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 (usually before first rc) ## Creating a new vMAJOR.MINOR branch (before first rc)
### In the new branch ### In the new branch
@@ -36,7 +36,7 @@
### Back in main ### Back in main
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat 1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
2. Start new /CHANGELOG.md 2. Start new /CHANGELOG.md
+3 -4
View File
@@ -12,16 +12,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.21 | Development 1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only 1.18 | Security patches only
1.17 | Security patches only 1.17 | Security patches only
1.16 | Security patches only 1.16 | Security patches only
1.15 | Security patches only
## Announcements ## Announcements
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub. You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
+1 -1
View File
@@ -1 +1 @@
1.21.0-dev 1.19.6
+1 -1
View File
@@ -6,7 +6,7 @@
set -e set -e
ELIXIR_VERSION=1.21.0-dev ELIXIR_VERSION=1.19.6
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2 cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team :: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec :: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.21.0-dev set ELIXIR_VERSION=1.19.6
if ""%1""=="""" if ""%2""=="""" goto documentation if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+2 -4
View File
@@ -293,7 +293,7 @@ defmodule EEx do
"foo baz" "foo baz"
""" """
@spec eval_string(String.t(), Code.binding(), [compile_opt]) :: term() @spec eval_string(String.t(), keyword, [compile_opt]) :: String.t()
def eval_string(source, bindings \\ [], options \\ []) def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options) compiled = compile_string(source, options)
@@ -315,7 +315,7 @@ defmodule EEx do
#=> "foo baz" #=> "foo baz"
""" """
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t() @spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
def eval_file(filename, bindings \\ [], options \\ []) def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename) filename = IO.chardata_to_string(filename)
@@ -344,7 +344,6 @@ defmodule EEx do
It returns `{:ok, [token]}` where a token is one of: It returns `{:ok, [token]}` where a token is one of:
* `{:comment, content, %{column: column, line: line}}`
* `{:text, content, %{column: column, line: line}}` * `{:text, content, %{column: column, line: line}}`
* `{:expr, marker, content, %{column: column, line: line}}` * `{:expr, marker, content, %{column: column, line: line}}`
* `{:start_expr, marker, content, %{column: column, line: line}}` * `{:start_expr, marker, content, %{column: column, line: line}}`
@@ -365,7 +364,6 @@ defmodule EEx do
### Helpers ### Helpers
defp do_eval(compiled, bindings, options) do defp do_eval(compiled, bindings, options) do
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
{result, _} = Code.eval_quoted(compiled, bindings, options) {result, _} = Code.eval_quoted(compiled, bindings, options)
result result
end end
+32 -74
View File
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
{:error, message <> code_snippet(state.source, state.indentation, meta), meta} {:error, message <> code_snippet(state.source, state.indentation, meta), meta}
{:ok, expr, new_line, new_column, rest} -> {:ok, expr, new_line, new_column, rest} ->
{key, expr, extra_meta} = {key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} -> {:ok, _line, _column, _warnings, rev_tokens, []} ->
# We ignore warnings because the code will be tokenized # We ignore warnings because the code will be tokenized
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
token_key(rev_tokens, expr) token_key(rev_tokens, expr)
{:error, _, _, _, _} -> {:error, _, _, _, _} ->
{:expr, expr, %{}} {:expr, expr}
end end
marker = marker =
@@ -96,14 +96,14 @@ defmodule EEx.Compiler do
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <> "unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
"please remove \"#{marker}\"" "please remove \"#{marker}\""
IO.warn(message, file: state.file, line: line, column: column) :elixir_errors.erl_warn({line, column}, state.file, message)
~c"" ~c""
else else
marker marker
end end
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)} token = {key, marker, expr, %{line: line, column: column}}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1)) trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
end end
end end
@@ -127,27 +127,6 @@ defmodule EEx.Compiler do
tokenize(rest, line, column, state, [{line, column}], fun.(acc)) tokenize(rest, line, column, state, [{line, column}], fun.(acc))
end end
# Merge middle expressions separated only by whitespace so the whitespace is
# part of the Elixir expression, not a separate EEx body.
defp merge_token(
{:middle_expr, ~c"", chars, meta},
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
) do
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
else
[
{:middle_expr, ~c"", chars, meta},
{:text, text, text_meta},
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
]
end
end
defp merge_token(token, acc) do
[token | acc]
end
# Retrieve marker for <% # Retrieve marker for <%
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
@@ -198,37 +177,35 @@ defmodule EEx.Compiler do
defp token_key(rev_tokens, expr) do defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
{[{:end, _} | _], [{:do, _} | _]} -> {[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr, %{}} {:middle_expr, expr}
{_, [{:do, _} | _]} -> {_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr), %{}} {:start_expr, maybe_append_space(expr)}
{_, [{:block_identifier, _, identifier} | _]} -> {_, [{:block_identifier, _, _} | _]} ->
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}} {:middle_expr, maybe_append_space(expr)}
{[{:end, _} | _], [{:stab_op, _, _} | _]} -> {[{:end, _} | _], [{:stab_op, _, _} | _]} ->
{:middle_expr, expr, %{}} {:middle_expr, expr}
{_, [{:stab_op, _, _} | rev_tokens]} -> {_, [{:stab_op, _, _} | reverse_tokens]} ->
if fn_before_end?(rev_tokens) do fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
{:start_expr, expr, %{}} end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if end_index > fn_index do
{:start_expr, expr}
else else
{:middle_expr, expr, %{}} {:middle_expr, expr}
end end
{tokens, _} -> {tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> {:end_expr, expr, %{}} [{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr, %{}} _ -> {:expr, expr}
end end
end end
end end
defp fn_before_end?([{:fn, _} | _]), do: true
defp fn_before_end?([{:end, _} | _]), do: false
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
defp fn_before_end?([]), do: false
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest) defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
defp drop_eol(rest), do: rest defp drop_eol(rest), do: rest
@@ -326,7 +303,7 @@ defmodule EEx.Compiler do
file: file, file: file,
source: source, source: source,
line: line, line: line,
quoted: %{}, quoted: [],
parser_options: [indentation: indentation] ++ parser_options, parser_options: [indentation: indentation] ++ parser_options,
indentation: indentation indentation: indentation
} }
@@ -370,7 +347,7 @@ defmodule EEx.Compiler do
state.parser_options state.parser_options
expr = Code.string_to_quoted!(chars, options) expr = Code.string_to_quoted!(chars, options)
buffer = handle_expr(buffer, mark, expr, meta, state) buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
generate_buffer(rest, buffer, scope, state) generate_buffer(rest, buffer, scope, state)
end end
@@ -389,17 +366,17 @@ defmodule EEx.Compiler do
rest, rest,
state.engine.handle_begin(buffer), state.engine.handle_begin(buffer),
[{contents, start_line, start_column} | scope], [{contents, start_line, start_column} | scope],
%{state | quoted: %{}, line: line} %{state | quoted: [], line: line}
) )
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message = message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\"" "the contents of this expression won't be output unless the EEx block starts with \"<%=\""
IO.warn(message, file: state.file, line: meta.line, column: meta.column) :elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end end
buffer = handle_expr(buffer, mark, contents, meta, state) buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state) generate_buffer(rest, buffer, scope, state)
end end
@@ -433,7 +410,7 @@ defmodule EEx.Compiler do
) do ) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state) {wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
options = [file: state.file, line: line, column: column] ++ state.parser_options options = [file: state.file, line: line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options) tuples = Code.string_to_quoted!(wrapped, options)
buffer = insert_quoted(tuples, state.quoted) buffer = insert_quoted(tuples, state.quoted)
{buffer, rest} {buffer, rest}
end end
@@ -449,7 +426,7 @@ defmodule EEx.Compiler do
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
message = "expected a closing '<% end %>' for block expression in EEx" message = "expected a closing '<% end %>' for block expression in EEx"
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state) expr_meta = non_whitespace_meta(content, line, column, state)
syntax_error!(message, expr_meta, state) syntax_error!(message, expr_meta, state)
end end
@@ -466,10 +443,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line) new_lines = List.duplicate(?\n, line - state.line)
key = map_size(state.quoted) key = length(state.quoted)
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"] placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
count = [current, placeholder, new_lines, chars] count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))} new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
{count, new_state} {count, new_state}
end end
@@ -499,16 +476,11 @@ defmodule EEx.Compiler do
Enum.all?(chars, &(&1 in @all_spaces)) Enum.all?(chars, &(&1 in @all_spaces))
end end
defp clause_block_identifier?(%{block_identifier: identifier}) do
identifier in [:else, :rescue, :catch]
end
defp clause_block_identifier?(_meta), do: false
# Changes placeholder to real expression # Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do defp insert_quoted({:__EEX__, _, [key]}, quoted) do
Map.fetch!(quoted, key) {^key, value} = List.keyfind(quoted, key, 0)
value
end end
defp insert_quoted({left, line, right}, quoted) do defp insert_quoted({left, line, right}, quoted) do
@@ -541,20 +513,6 @@ defmodule EEx.Compiler do
column: meta.column column: meta.column
end end
defp handle_expr(buffer, mark, expr, meta, state) do
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
rescue
e in EEx.SyntaxError ->
reraise %{
e
| file: e.file || state.file,
line: e.line || meta.line,
column: e.column || meta.column,
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
},
__STACKTRACE__
end
defp code_snippet(source, indentation, meta) do defp code_snippet(source, indentation, meta) do
line_start = max(meta.line - 3, 1) line_start = max(meta.line - 3, 1)
line_end = meta.line line_end = meta.line
+1 -1
View File
@@ -195,7 +195,7 @@ defmodule EEx.Engine do
def handle_expr(state, "=", ast) do def handle_expr(state, "=", ast) do
check_state!(state) check_state!(state)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state %{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__) var = Macro.var(:"arg#{vars_count}", __MODULE__)
ast = ast =
quote do quote do
+2 -2
View File
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
{:text, ~c"foo ", %{column: 1, line: 1}}, {:text, ~c"foo ", %{column: 1, line: 1}},
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}}, {:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
{:text, ~c"bar", %{column: 21, line: 1}}, {:text, ~c"bar", %{column: 21, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}}, {:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
{:text, ~c"baz", %{column: 34, line: 1}}, {:text, ~c"baz", %{column: 34, line: 1}},
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}}, {:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
{:eof, %{column: 46, line: 1}} {:eof, %{column: 46, line: 1}}
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
exprs = [ exprs = [
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}}, {:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}}, {:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}}, {:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}}, {:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}}, {:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
{:eof, %{column: 3, line: 7}} {:eof, %{column: 3, line: 7}}
+1 -44
View File
@@ -262,37 +262,6 @@ defmodule EExTest do
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>") assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
end end
test "with expression with else clause split across tags" do
template = """
<%= with {:ok, x} <- @res do %>
<p><%= x %></p>
<% else %>
<% _ -> %>
<p>bad</p>
<% end %>
"""
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
engine: EEx.SmartEngine
)
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
engine: EEx.SmartEngine
)
end
test "empty clauses separated by whitespace" do
template = """
<%= case x do %>
<% :foo -> %>
<% :bar -> %>
<% end %>
"""
assert_eval("\n \n", template, x: :foo)
assert_eval("\n\n", template, x: :bar)
end
test "with end of token" do test "with end of token" do
assert_eval("foo bar %>", "foo bar %>") assert_eval("foo bar %>", "foo bar %>")
end end
@@ -553,7 +522,7 @@ defmodule EExTest do
assert message |> Exception.message() |> strip_ansi() =~ """ assert message |> Exception.message() |> strip_ansi() =~ """
│ │
#{line + 2} │ true && @some[\s 514 │ true && @some[\s
│ │ └ missing closing delimiter (expected "]") │ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter │ └ unclosed delimiter
""" """
@@ -606,18 +575,6 @@ defmodule EExTest do
EEx.compile_string("foo <%= bar", file: "my_file.eex") EEx.compile_string("foo <%= bar", file: "my_file.eex")
end end
end end
test "unsupported marker error carries template location metadata" do
error =
assert_raise EEx.SyntaxError, fn ->
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
end
assert error.file == "sample.eex"
assert error.line == 7
assert error.column == 1
assert Exception.message(error) =~ "sample.eex:7:1:"
end
end end
describe "warnings" do describe "warnings" do
+5 -10
View File
@@ -8,13 +8,8 @@
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__) Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("eex") CoverageRecorder.maybe_record("eex")
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: [] ExUnit.start(
trace: !!System.get_env("TRACE"),
ex_unit_opts = include: line_include,
[ exclude: line_exclude
trace: !!System.get_env("TRACE"), )
include: line_include,
exclude: line_exclude
] ++ maybe_seed_opt
ExUnit.start(ex_unit_opts)
+39 -90
View File
@@ -10,8 +10,8 @@ defmodule Access do
keys of any type in a data structure via the `data[key]` syntax. keys of any type in a data structure via the `data[key]` syntax.
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out `Access` supports keyword lists (`Keyword`) and maps (`Map`) out
of the box. Keyword lists support only atom keys, while keys for maps of the box. Keywords supports only atoms keys, keys for maps can
can be of any type. Both return `nil` if the key does not exist: be of any type. Both return `nil` if the key does not exist:
iex> keywords = [a: 1, b: 2] iex> keywords = [a: 1, b: 2]
iex> keywords[:a] iex> keywords[:a]
@@ -33,7 +33,7 @@ defmodule Access do
iex> keywords[:c][:unknown] iex> keywords[:c][:unknown]
nil nil
This works because accessing anything on a `nil` value returns This works because accessing anything on a `nil` value, returns
`nil` itself: `nil` itself:
iex> nil[:a] iex> nil[:a]
@@ -226,8 +226,6 @@ defmodule Access do
end end
end end
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
@doc """ @doc """
Fetches the value for the given key in a container (a map, keyword Fetches the value for the given key in a container (a map, keyword
list, or struct that implements the `Access` behaviour). list, or struct that implements the `Access` behaviour).
@@ -487,7 +485,7 @@ defmodule Access do
## Accessors ## Accessors
@doc """ @doc """
Returns a function that accesses the given key in a map/struct/keyword list. Returns a function that accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`, The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends. `Kernel.get_and_update_in/3`, and friends.
@@ -516,56 +514,30 @@ defmodule Access do
iex> pop_in(map, [Access.key(:user), Access.key(:name)]) iex> pop_in(map, [Access.key(:user), Access.key(:name)])
{"john", %{user: %{}}} {"john", %{user: %{}}}
iex> keyword = [user: [name: "john"]] An error is raised if the accessed structure is not a map or a struct:
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
"john"
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
{"john", [user: []]}
An error is raised if the accessed structure is not a map, struct, or keyword list: iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got:
iex> get_in(123, [Access.key(:foo)]) ...
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
""" """
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term) @spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
def key(key, default \\ nil) do def key(key, default \\ nil) do
fn fn
:get, %{} = data, next -> :get, data, next ->
next.(Map.get(data, key, default)) next.(Map.get(data, key, default))
:get_and_update, %{} = data, next -> :get_and_update, data, next ->
value = Map.get(data, key, default) value = Map.get(data, key, default)
case next.(value) do case next.(value) do
{get, update} -> {get, Map.put(data, key, update)} {get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)} :pop -> {value, Map.delete(data, key)}
end end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.get(data, key, default))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.get(data, key, default)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next ->
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
end end
end end
@doc """ @doc """
Returns a function that accesses the given key in a map/struct/keyword list. Returns a function that accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`, The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends. `Kernel.get_and_update_in/3`, and friends.
@@ -574,19 +546,6 @@ defmodule Access do
## Examples ## Examples
iex> keyword = [user: [name: "john"]]
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
"john"
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
{"john", [user: []]}
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in:
...
iex> map = %{user: %{name: "john"}} iex> map = %{user: %{name: "john"}}
iex> get_in(map, [Access.key!(:user), Access.key!(:name)]) iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
"john" "john"
@@ -615,15 +574,13 @@ defmodule Access do
`Access.key!/1` is useful when the key is not known in advance `Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically. and must be accessed dynamically.
An error is raised if the accessed structure is not a map/struct/keyword list: An error is raised if the accessed structure is not a map/struct:
iex> get_in(123, [Access.key!(:foo)]) iex> get_in([], [Access.key!(:foo)])
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123 ** (RuntimeError) Access.key!/1 expected a map/struct, got: []
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
""" """
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term) @spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
def key!(key) do def key!(key) do
fn fn
:get, %{} = data, next -> :get, %{} = data, next ->
@@ -637,19 +594,8 @@ defmodule Access do
:pop -> {value, Map.delete(data, key)} :pop -> {value, Map.delete(data, key)}
end end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.fetch!(data, key))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.fetch!(data, key)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next -> _op, data, _next ->
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}" raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
end end
end end
@@ -917,7 +863,7 @@ defmodule Access do
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name]) iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]} {["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
When no match is found, an empty list is returned and the update function is never called: When no match is found, an empty list is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}] iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name]) iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
@@ -927,6 +873,11 @@ defmodule Access do
...> end) ...> end)
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]} {[], [%{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.filter(5)])
** (FunctionClauseError) no function clause matching in Access.filter/1
An error is raised if the accessed structure is not a list: An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)]) iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
@@ -934,7 +885,7 @@ defmodule Access do
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list) @spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
def filter(func) when is_function(func) do def filter(func) when is_function(func) do
fn op, data, next -> filter(op, data, func, next) end fn op, data, next -> filter(op, data, func, next) end
end end
@@ -1035,12 +986,12 @@ defmodule Access do
end end
defp slice(:get_and_update, data, range, next) when is_list(data) do defp slice(:get_and_update, data, range, next) when is_list(data) do
%Range{first: first, last: last, step: step} = normalize_range(range, data) range = normalize_range(range, data)
if first > last do if range.first > range.last do
{[], data} {[], data}
else else
get_and_update_slice(data, first, last, step, next, [], [], 0) get_and_update_slice(data, range, next, [], [], 0)
end end
end end
@@ -1145,23 +1096,16 @@ defmodule Access do
defp normalize_range(range, _list), do: range defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index) defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
when index > last do if index in range do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
if index >= first and rem(index - first, step) == 0 do
case next.(head) do case next.(head) do
:pop -> :pop ->
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1) get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
{get, update} -> {get, update} ->
get_and_update_slice( get_and_update_slice(
rest, rest,
first, range,
last,
step,
next, next,
[update | updates], [update | updates],
[get | gets], [get | gets],
@@ -1169,11 +1113,11 @@ defmodule Access do
) )
end end
else else
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1) get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
end end
end end
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)} {:lists.reverse(gets), :lists.reverse(updates)}
end end
@@ -1200,7 +1144,7 @@ defmodule Access do
iex> pop_in(list, [Access.find(&(&1.salary <= 40))]) iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]} {%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called: 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> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name]) iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
@@ -1210,6 +1154,11 @@ defmodule Access do
...> end) ...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]} {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: An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)]) iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
+3 -3
View File
@@ -327,7 +327,7 @@ defmodule Agent do
passing the agent state. The result of the function invocation is passing the agent state. The result of the function invocation is
returned from this function. returned from this function.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
@@ -366,7 +366,7 @@ defmodule Agent do
elements, the first being the value to return (that is, the "get" value) elements, the first being the value to return (that is, the "get" value)
and the second one being the new state of the agent. and the second one being the new state of the agent.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
@@ -407,7 +407,7 @@ defmodule Agent do
This function always returns `:ok`. This function always returns `:ok`.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
+15 -21
View File
@@ -507,7 +507,7 @@ defmodule Application do
of all loaded applications. Returns `nil` if of all loaded applications. Returns `nil` if
the module is not listed in any application spec. the module is not listed in any application spec.
""" """
@spec get_application(module) :: app | nil @spec get_application(atom) :: atom | nil
def get_application(module) when is_atom(module) do def get_application(module) when is_atom(module) do
case :application.get_application(module) do case :application.get_application(module) do
{:ok, app} -> app {:ok, app} -> app
@@ -680,13 +680,13 @@ defmodule Application do
## Examples ## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications. `get_env/3` is commonly used to read the configuration of your OTP applications.
Since Mix configurations are commonly used to configure applications (including Since Mix configurations are commonly used to configure applications, we will use
your dependencies), we will use this as a point of illustration. this as a point of illustration.
Consider a new application `:my_app`. `:my_app` contains a database engine which Consider a new application `:my_app`. `:my_app` contains a database engine which
supports a pool of databases. The database engine needs to know the configuration for supports a pool of databases. The database engine needs to know the configuration for
each of those databases, and that configuration is supplied by key-value pairs in each of those databases, and that configuration is supplied by key-value pairs in
environment of `:my_app`. For example, your `config/runtime.exs` file might have: environment of `:my_app`.
config :my_app, Databases.RepoOne, config :my_app, Databases.RepoOne,
# A database configuration # A database configuration
@@ -696,7 +696,7 @@ defmodule Application do
config :my_app, Databases.RepoTwo, config :my_app, Databases.RepoTwo,
# Another database configuration (for the same OTP app) # Another database configuration (for the same OTP app)
ip: "localhost", ip: "localhost",
port: 20_717 port: 20717
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo] config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
@@ -714,11 +714,6 @@ defmodule Application do
config = Application.get_env(:my_app, Databases.RepoOne) config = Application.get_env(:my_app, Databases.RepoOne)
config[:ip] config[:ip]
The sample `config/runtime.exs` above could be used both for `:my_app` to
configure itself but also to allow any application that depends on `:my_app`
to configure how it works. However, one should keep in mind the caveats described
in the `Application` module documentation: the application environment is global
state which should be avoided if possible.
""" """
@spec get_env(app, key, value) :: value @spec get_env(app, key, value) :: value
def get_env(app, key, default \\ nil) when is_atom(app) do def get_env(app, key, default \\ nil) when is_atom(app) do
@@ -819,7 +814,7 @@ defmodule Application do
stick after the application is loaded and also on application reload. stick after the application is loaded and also on application reload.
""" """
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok @spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do def put_env(app, key, value, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key) maybe_warn_on_app_env_key(app, key)
:application.set_env(app, key, value, opts) :application.set_env(app, key, value, opts)
end end
@@ -861,7 +856,7 @@ defmodule Application do
It receives the same options as `put_env/4`. Returns `:ok`. It receives the same options as `put_env/4`. Returns `:ok`.
""" """
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok @spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do def delete_env(app, key, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key) maybe_warn_on_app_env_key(app, key)
:application.unset_env(app, key, opts) :application.unset_env(app, key, opts)
end end
@@ -908,13 +903,13 @@ defmodule Application do
@doc """ @doc """
Ensures the given `app` or `apps` and their child applications are started. Ensures the given `app` or `apps` and their child applications are started.
The second argument is either the `t:restart_type/0` (for consistency with The second argument is either the `t:restart_type/1` (for consistency with
`start/2`) or a keyword list. `start/2`) or a keyword list.
## Options ## Options
* `:type` - if the application should be started `:temporary` (default), * `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/0` for more information. `:permanent`, or `:transient`. See `t:restart_type/1` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially * `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`). (`:serial`, default) or concurrently (`:concurrent`).
@@ -926,11 +921,11 @@ defmodule Application do
{:ok, [app]} | {:error, term} {:ok, [app]} | {:error, term}
def ensure_all_started(app_or_apps, type_or_opts \\ []) def ensure_all_started(app_or_apps, type_or_opts \\ [])
def ensure_all_started(app_or_apps, type) when is_atom(type) do def ensure_all_started(app, type) when is_atom(type) do
ensure_all_started(app_or_apps, type: type) ensure_all_started(app, type: type)
end end
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do def ensure_all_started(app, opts) when is_atom(app) do
ensure_all_started([app], opts) ensure_all_started([app], opts)
end end
@@ -996,7 +991,7 @@ defmodule Application do
end end
@doc """ @doc """
Gets the directory for `app`. Gets the directory for app.
This information is returned based on the code path. Here is an This information is returned based on the code path. Here is an
example: example:
@@ -1061,8 +1056,7 @@ defmodule Application do
Returns a list with information about the applications which are currently running. Returns a list with information about the applications which are currently running.
""" """
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}] @spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
def started_applications(timeout \\ 5000) def started_applications(timeout \\ 5000) do
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
:application.which_applications(timeout) :application.which_applications(timeout)
end end
@@ -1077,7 +1071,7 @@ defmodule Application do
@doc """ @doc """
Formats the error reason returned by `start/2`, Formats the error reason returned by `start/2`,
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`, `ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
and returns a string. returns a string.
""" """
@spec format_error(any) :: String.t() @spec format_error(any) :: String.t()
def format_error(reason) do def format_error(reason) do
+70 -294
View File
@@ -155,255 +155,6 @@ defmodule Base do
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>> for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
end end
# SWAR (SIMD Within A Register) fast paths for valid16?/2 and valid32?/2
# (non-hex). Each chunk of 8 bytes is validated in one guard: 7 bytes via
# bitwise arithmetic on a single 56-bit integer, plus a per-byte range
# check for the 8th byte. 56 bits is the largest width that fits in a BEAM
# small int on 64-bit (fixnum range is 59-bit signed); at 64 bits every
# `w + 0x80..` would allocate a bignum on the heap and the optimisation
# would collapse. See https://github.com/erlang/otp/pull/10938 for the
# corresponding pattern in OTP.
@swar_mask80 0x80808080808080
# Per-range SWAR constants, broadcast across 7 lanes. Naming convention:
# @swar_ge_X = 0x80 - X → high bit of `(w + @swar_ge_X)` lane is set
# iff that byte is ≥ X
# @swar_gt_X = 0x7F - X → high bit of `(w + @swar_gt_X)` lane is set
# iff that byte is > X
# A byte is in range [lo, hi] iff
# `bxor(w + @swar_ge_lo, w + @swar_gt_hi)` has its high bit set.
@swar_ge_0 0x50505050505050
@swar_gt_9 0x46464646464646
@swar_ge_2 0x4E4E4E4E4E4E4E
@swar_gt_7 0x48484848484848
@swar_ge_A 0x3F3F3F3F3F3F3F
@swar_gt_F 0x39393939393939
@swar_gt_V 0x29292929292929
@swar_gt_Z 0x25252525252525
@swar_ge_a 0x1F1F1F1F1F1F1F
@swar_gt_f 0x19191919191919
@swar_gt_v 0x09090909090909
@swar_gt_z 0x05050505050505
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
# check — saves one singleton term. Trick lifted from
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
@swar_ge_slash 0x51515151515151
# base64's '+', '-' and '_' border no other range, so each is checked as the
# degenerate range [c, c]. Mycroft zero-byte detection is one operation
# cheaper per singleton but not exact per lane: subtracting 0x01..01 makes a
# zero lane borrow from the lane above it, and a lane holding 1 turns that
# borrow into 0xFF, so bytes equal to `c bxor 0x01` read as matches too.
@swar_ge_plus 0x55555555555555
@swar_gt_plus 0x54545454545454
@swar_ge_dash 0x53535353535353
@swar_gt_dash 0x52525252525252
@swar_ge_underscore 0x21212121212121
@swar_gt_underscore 0x20202020202020
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
# of each stride and in the body of the sub-8-byte tail clauses).
@compile {:inline,
valid_char16upper?: 1,
valid_char16lower?: 1,
valid_char16mixed?: 1,
valid_char32upper?: 1,
valid_char32lower?: 1,
valid_char32mixed?: 1,
valid_char32hexupper?: 1,
valid_char32hexlower?: 1,
valid_char32hexmixed?: 1,
valid_char64base?: 1,
valid_char64url?: 1,
valid_word16upper?: 1,
valid_word16lower?: 1,
valid_word16mixed?: 1,
valid_word32upper?: 1,
valid_word32lower?: 1,
valid_word32mixed?: 1,
valid_word32hexupper?: 1,
valid_word32hexlower?: 1,
valid_word32hexmixed?: 1,
valid_word64base?: 1,
valid_word64url?: 1}
defp valid_char16upper?(c), do: c in ?0..?9 or c in ?A..?F
defp valid_char16lower?(c), do: c in ?0..?9 or c in ?a..?f
defp valid_char16mixed?(c), do: c in ?0..?9 or c in ?A..?F or c in ?a..?f
defp valid_char32upper?(c), do: c in ?A..?Z or c in ?2..?7
defp valid_char32lower?(c), do: c in ?a..?z or c in ?2..?7
defp valid_char32mixed?(c), do: c in ?A..?Z or c in ?a..?z or c in ?2..?7
# Most common range first — letters dominate (22/32) over digits (10/32)
# in hex base32, so letters go first in the OR short-circuit.
defp valid_char32hexupper?(c), do: c in ?A..?V or c in ?0..?9
defp valid_char32hexlower?(c), do: c in ?a..?v or c in ?0..?9
defp valid_char32hexmixed?(c), do: c in ?A..?V or c in ?a..?v or c in ?0..?9
defp valid_char64base?(c),
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?+ or c == ?/
defp valid_char64url?(c),
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?- or c == ?_
# SWAR 7-byte word validity. Structure for each function:
# 1. ASCII gate `band(w, MASK80) == 0` — every byte < 0x80 so the
# additions below cannot carry across lanes.
# 2. "Each byte is in range A OR range B (OR range C)" gate — OR per-
# range XOR masks (high bit set in lane iff byte in that range), AND
# with MASK80, demand all 7 high bits set.
defp valid_word16upper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_F)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word16lower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_a, w + @swar_gt_f)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word16mixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_F)
),
bxor(w + @swar_ge_a, w + @swar_gt_f)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32upper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32lower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_a, w + @swar_gt_z),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32mixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexupper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_V)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexlower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_a, w + @swar_gt_v)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexmixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_V)
),
bxor(w + @swar_ge_a, w + @swar_gt_v)
),
@swar_mask80
) == @swar_mask80
end
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
# part of one range (Lemire merge), leaving only '+' as a singleton. For url,
# '-' and '_' are two more singletons.
defp valid_word64base?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_slash, w + @swar_gt_9)
),
bxor(w + @swar_ge_plus, w + @swar_gt_plus)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word64url?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_0, w + @swar_gt_9)
),
bor(
bxor(w + @swar_ge_dash, w + @swar_gt_dash),
bxor(w + @swar_ge_underscore, w + @swar_gt_underscore)
)
),
@swar_mask80
) == @swar_mask80
end
@doc """ @doc """
Encodes a binary string into a base 16 encoded string. Encodes a binary string into a base 16 encoded string.
@@ -436,7 +187,7 @@ defmodule Base do
end end
for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do
name = String.to_unsafe_atom("encode16#{base}") name = :"encode16#{base}"
encoded = to_encode_list.(alphabet) encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]} @compile {:inline, [{name, 1}]}
@@ -536,7 +287,7 @@ defmodule Base do
* `:lower` - only allows lower case characters * `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters * `:mixed` - allows mixed case characters
An `ArgumentError` exception is raised if the string has an odd length or An `ArgumentError` exception is raised if the padding is incorrect or
a non-alphabet character is present in the string. a non-alphabet character is present in the string.
## Examples ## Examples
@@ -617,24 +368,28 @@ defmodule Base do
upper = Enum.with_index(b16_alphabet) upper = Enum.with_index(b16_alphabet)
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
decode_name = String.to_unsafe_atom("decode16#{base}!") decode_name = :"decode16#{base}!"
validate_name = String.to_unsafe_atom("validate16#{base}?") validate_name = :"validate16#{base}?"
valid_char_name = String.to_unsafe_atom("valid_char16#{base}?") valid_char_name = :"valid_char16#{base}?"
valid_word_name = String.to_unsafe_atom("valid_word16#{base}?")
{min, decoded} = to_decode_list.(alphabet) {min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated entirely via
# `valid_word16<base>?` in the body. The `and` short-circuits when SWAR
# fails on any byte. Tail bytes (1-6 leftover) recurse through the
# single-byte clause below.
defp unquote(validate_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_name)(rest)
defp unquote(validate_name)(<<>>), do: true defp unquote(validate_name)(<<>>), do: true
defp unquote(validate_name)(<<char, rest::binary>>), defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest) unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(validate_name)(rest)
end
defp unquote(validate_name)(<<_char, _rest::binary>>), do: false
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do defp unquote(decode_name)(char) do
index = char - unquote(min) index = char - unquote(min)
@@ -735,7 +490,7 @@ defmodule Base do
end end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
name = String.to_unsafe_atom("encode64#{base}") name = :"encode64#{base}"
encoded = to_encode_list.(alphabet) encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]} @compile {:inline, [{name, 1}]}
@@ -1001,24 +756,28 @@ defmodule Base do
end end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
decode_name = String.to_unsafe_atom("decode64#{base}!") decode_name = :"decode64#{base}!"
validate_name = String.to_unsafe_atom("validate64#{base}?") validate_name = :"validate64#{base}?"
validate_main_name = String.to_unsafe_atom("validate_main64#{validate_name}?") validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = String.to_unsafe_atom("valid_char64#{base}?") valid_char_name = :"valid_char64#{base}?"
valid_word_name = String.to_unsafe_atom("valid_word64#{base}?")
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.() {min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause.
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(<<char, rest::binary>>), defp unquote(validate_main_name)(
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest) <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_name)(<<>>, _pad?), do: true defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -1104,6 +863,13 @@ defmodule Base do
end end
end end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do defp unquote(decode_name)(char) do
index = char - unquote(min) index = char - unquote(min)
@@ -1314,7 +1080,7 @@ defmodule Base do
hexupper: b32hex_alphabet, hexupper: b32hex_alphabet,
hexlower: to_lower_enc.(b32hex_alphabet) hexlower: to_lower_enc.(b32hex_alphabet)
] do ] do
name = String.to_unsafe_atom("encode32#{base}") name = :"encode32#{base}"
encoded = to_encode_list.(alphabet) encoded = to_encode_list.(alphabet)
@compile {:inline, [{name, 1}]} @compile {:inline, [{name, 1}]}
@@ -1388,7 +1154,7 @@ defmodule Base do
The values for `:case` can be: The values for `:case` can be:
* `:upper` - only allows upper case characters (default) * `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters * `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters * `:mixed` - allows mixed case characters
@@ -1653,24 +1419,27 @@ defmodule Base do
hexlower: to_lower_dec.(hexupper), hexlower: to_lower_dec.(hexupper),
hexmixed: to_mixed_dec.(hexupper) hexmixed: to_mixed_dec.(hexupper)
] do ] do
decode_name = String.to_unsafe_atom("decode32#{base}!") decode_name = :"decode32#{base}!"
validate_name = String.to_unsafe_atom("validate32#{base}?") validate_name = :"validate32#{base}?"
validate_main_name = String.to_unsafe_atom("validate_main32#{validate_name}?") validate_main_name = :"validate_main32#{validate_name}?"
valid_char_name = String.to_unsafe_atom("valid_char32#{base}?") valid_char_name = :"valid_char32#{base}?"
{min, decoded} = to_decode_list.(alphabet) {min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause.
valid_word_name = String.to_unsafe_atom("valid_word32#{base}?")
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(<<char, rest::binary>>), defp unquote(validate_main_name)(
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest) <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_name)(<<>>, _pad?), do: true defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -1750,6 +1519,13 @@ defmodule Base do
end end
end end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do defp unquote(decode_name)(char) do
index = char - unquote(min) index = char - unquote(min)
+29 -56
View File
@@ -3,8 +3,6 @@
# SPDX-FileCopyrightText: 2012 Plataformatec # SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar do defmodule Calendar do
@strftime_max_width 1024
@moduledoc """ @moduledoc """
This module defines the responsibilities for working with This module defines the responsibilities for working with
calendars, dates, times and datetimes in Elixir. calendars, dates, times and datetimes in Elixir.
@@ -106,7 +104,6 @@ defmodule Calendar do
@typedoc "Any map or struct that contains the time fields." @typedoc "Any map or struct that contains the time fields."
@type time :: %{ @type time :: %{
optional(any) => any, optional(any) => any,
calendar: calendar,
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, second: second,
@@ -296,7 +293,7 @@ defmodule Calendar do
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond} @callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
@doc """ @doc """
Defines the rollover moment for the calendar. Define the rollover moment for the calendar.
This is the moment, in your calendar, when the current day ends This is the moment, in your calendar, when the current day ends
and the next day starts. and the next day starts.
@@ -382,13 +379,13 @@ defmodule Calendar do
@callback iso_days_to_end_of_day(iso_days) :: iso_days @callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """ @doc """
Shifts date by the given duration according to its calendar. Shifts date by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day} @callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """ @doc """
Shifts naive datetime by the given duration according to its calendar. Shifts naive datetime by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_naive_datetime( @callback shift_naive_datetime(
@@ -403,7 +400,7 @@ defmodule Calendar do
) :: {year, month, day, hour, minute, second, microsecond} ) :: {year, month, day, hour, minute, second, microsecond}
@doc """ @doc """
Shifts time by the given duration according to its calendar. Shifts time by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) :: @callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
@@ -532,7 +529,6 @@ defmodule Calendar do
* `%`: indicates the start of a formatted section * `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below) * `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section * `<width>`: a number indicating the minimum size of the formatted section
(maximum #{@strftime_max_width})
* `<format>`: the format itself (see below) * `<format>`: the format itself (see below)
### Accepted padding options ### Accepted padding options
@@ -563,11 +559,11 @@ defmodule Calendar do
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
q | Quarter | 1, 2, 3, 4 q | Quarter | 1, 2, 3, 4
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877 s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
S | Second | 00, 59 S | Second | 00, 59, 60
u | Day of the week | 1 (Monday), 7 (Sunday) u | Day of the week | 1 (Monday), 7 (Sunday)
x | Preferred date (without time) representation | 2018-10-17 x | Preferred date (without time) representation | 2018-10-17
X | Preferred time (without date) representation | 12:34:56 X | Preferred time (without date) representation | 12:34:56
y | Year as 2-digits | -01, 01, 86, 18 y | Year as 2-digits | 01, 01, 86, 18
Y | Year | -0001, 0001, 1986 Y | Year | -0001, 0001, 1986
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530 z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
Z | Time zone abbreviation (empty string if naive) | CET, BRST Z | Time zone abbreviation (empty string if naive) | CET, BRST
@@ -624,7 +620,7 @@ defmodule Calendar do
...>) ...>)
"серпень" "серпень"
Microsecond formatting: Microsecond formatting:
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f") iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.0" "19-08-26 13:52:06.0"
@@ -671,13 +667,9 @@ defmodule Calendar do
end end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
width = (width || 0) * 10 + (digit - ?0) new_width = (width || 0) * 10 + (digit - ?0)
if width > @strftime_max_width do parse_modifiers(rest, new_width, pad, parser_data)
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
end
parse_modifiers(rest, width, pad, parser_data)
end end
# set default padding if none was specified # set default padding if none was specified
@@ -712,7 +704,7 @@ defmodule Calendar do
# Literally just % # Literally just %
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc]) parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
end end
# Abbreviated name of day # Abbreviated name of day
@@ -781,7 +773,7 @@ defmodule Calendar do
# Day of the month # Day of the month
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -800,35 +792,31 @@ defmodule Calendar do
# Hour using a 24-hour clock # Hour using a 24-hour clock
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Hour using a 12-hour clock # Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Day of the year # Day of the year
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Month # Month
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Minute # Minute
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -856,23 +844,19 @@ defmodule Calendar do
# Quarter # Quarter
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Second # Second
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Day of the week # Day of the week
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -922,25 +906,20 @@ defmodule Calendar do
# Year as 2-digits # Year as 2-digits
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
if datetime.year < 0 do
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
else
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
end
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Year # Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = {sign, year} =
if datetime.year < 0 do if datetime.year < 0 do
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)] {?-, -datetime.year}
else else
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad) {[], datetime.year}
end end
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -987,7 +966,7 @@ defmodule Calendar do
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60)) Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
sign = if utc_offset + std_offset >= 0, do: "+", else: "-" sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}" result = "#{sign}#{pad_leading(offset_number, width, pad)}"
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -1006,19 +985,13 @@ defmodule Calendar do
raise ArgumentError, "invalid strftime format: %#{next}" raise ArgumentError, "invalid strftime format: %#{next}"
end end
defp pad_preferred(result, width, pad) do defp pad_preferred(result, width, pad) when length(result) < width do
result pad_preferred([pad | result], width, pad)
|> IO.iodata_to_binary()
|> pad_leading(width, pad)
end end
defp pad_preferred(result, _width, _pad), do: result
defp pad_leading(string, count, padding) do defp pad_leading(string, count, padding) do
to_pad = count - String.length(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
defp pad_leading_ascii(string, count, padding) do
to_pad = count - byte_size(string) to_pad = count - byte_size(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end end
+13 -17
View File
@@ -53,7 +53,7 @@ defmodule Date do
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01]) iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
14716 14716
iex> Date.add(~D[1970-01-01], 14_716) iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17] ~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2) iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
@@ -81,7 +81,7 @@ defmodule Date do
Ranges of dates can be increasing (`first <= last`) and are Ranges of dates can be increasing (`first <= last`) and are
always inclusive. For a decreasing range, use `range/3` with always inclusive. For a decreasing range, use `range/3` with
a step of -1 as third argument. a step of -1 as first argument.
## Examples ## Examples
@@ -160,7 +160,7 @@ defmodule Date do
) do ) do
raise ArgumentError, raise ArgumentError,
"both dates must have matching calendar and the step must be a " <> "both dates must have matching calendar and the step must be a " <>
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}" "non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
end end
defp range(first, first_days, last, last_days, calendar, step) do defp range(first, first_days, last, last_days, calendar, step) do
@@ -422,7 +422,7 @@ defmodule Date do
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
date date
|> convert!(Calendar.ISO) |> convert!(Calendar.ISO)
|> to_iso8601(format) |> to_iso8601()
end end
@doc """ @doc """
@@ -556,18 +556,14 @@ defmodule Date do
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt @spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
def compare( def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
%{year: year1, month: month1, day: day1, calendar: calendar}, %{year: year1, month: month1, day: day1} = date1
%{year: year2, month: month2, day: day2, calendar: calendar} %{year: year2, month: month2, day: day2} = date2
) do
cond do case {{year1, month1, day1}, {year2, month2, day2}} do
year1 > year2 -> :gt {first, second} when first > second -> :gt
year1 < year2 -> :lt {first, second} when first < second -> :lt
month1 > month2 -> :gt _ -> :eq
month1 < month2 -> :lt
day1 > day2 -> :gt
day1 < day2 -> :lt
true -> :eq
end end
end end
@@ -1051,7 +1047,7 @@ defmodule Date do
@doc """ @doc """
Calculates the quarter of the year of a given `date`. Calculates the quarter of the year of a given `date`.
Returns the quarter of the year as an integer. For the ISO 8601 Returns the day of the year as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 4. calendar (the default), it is an integer from 1 to 4.
## Examples ## Examples
+46 -26
View File
@@ -37,19 +37,21 @@ defmodule Date.Range do
first_in_iso_days: first_days, first_in_iso_days: first_days,
last_in_iso_days: last_days, last_in_iso_days: last_days,
step: step step: step
}, } = range,
%Date{calendar: calendar} = date %Date{calendar: calendar} = date
) do ) do
{days, _} = Date.to_iso_days(date) {days, _} = Date.to_iso_days(date)
in_range? = cond do
if step > 0 do empty?(range) ->
first_days <= days and days <= last_days and rem(days - first_days, step) == 0 {:ok, false}
else
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
end
{:ok, in_range?} first_days <= last_days ->
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
true ->
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
end
end end
def member?(%Date.Range{step: _}, _) do def member?(%Date.Range{step: _}, _) do
@@ -57,20 +59,11 @@ defmodule Date.Range do
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
member? = def member?(
quote generated: true do %{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
member?( date_range,
%{ date
__struct__: Date.Range, ) do
first_in_iso_days: var!(first_days),
last_in_iso_days: var!(last_days)
} =
var!(date_range),
var!(date)
)
end
def unquote(member?) do
step = if first_days <= last_days, do: 1, else: -1 step = if first_days <= last_days, do: 1, else: -1
member?(Map.put(date_range, :step, step), date) member?(Map.put(date_range, :step, step), date)
end end
@@ -86,7 +79,7 @@ defmodule Date.Range do
step: step step: step
} = range } = range
) do ) do
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)} {:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
@@ -183,7 +176,7 @@ defmodule Date.Range do
last_in_iso_days: last_days, last_in_iso_days: last_days,
step: step step: step
}), }),
do: div(last_days - first_days, step) + 1 do: abs(div(last_days - first_days, step)) + 1
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
defp size( defp size(
@@ -193,16 +186,43 @@ defmodule Date.Range do
step = if first_days <= last_days, do: 1, else: -1 step = if first_days <= last_days, do: 1, else: -1
size(Map.put(date_range, :step, step)) size(Map.put(date_range, :step, step))
end end
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step > 0 and first_days > last_days,
do: true
defp empty?(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
})
when step < 0 and first_days < last_days,
do: true
defp empty?(%Date.Range{step: _}), do: false
# TODO: Remove me on v2.0
defp empty?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
empty?(Map.put(date_range, :step, step))
end
end end
defimpl Inspect do defimpl Inspect do
import Kernel, except: [inspect: 2] import Kernel, except: [inspect: 2]
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")" "Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
end end
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do def inspect(%Date.Range{first: first, last: last, step: step}, _) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})" "Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
end end
+49 -44
View File
@@ -180,7 +180,7 @@ defmodule DateTime do
since v1.15.0. since v1.15.0.
The default unit if none gets passed is `:native`, The default unit if none gets passed is `:native`,
which results in a default resolution of microseconds. which results on a default resolution of microseconds.
## Examples ## Examples
@@ -720,9 +720,6 @@ defmodule DateTime do
Other time zone databases can be passed as argument or set globally. Other time zone databases can be passed as argument or set globally.
See the "Time zone database" section in the module docs. See the "Time zone database" section in the module docs.
Shifting to the `"Etc/UTC"` time zone always succeeds without
consulting the `time_zone_database`.
## Examples ## Examples
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase) iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
@@ -756,28 +753,6 @@ defmodule DateTime do
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database) |> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
end end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days_utc)
datetime = %DateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision},
std_offset: 0,
utc_offset: 0,
zone_abbr: "UTC",
time_zone: "Etc/UTC"
}
{:ok, datetime}
end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} -> {:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
@@ -1202,7 +1177,7 @@ defmodule DateTime do
datetime datetime
|> to_iso_days() |> to_iso_days()
# Subtract total original offset in order to get UTC and add the new offset # Subtract total original offset in order to get UTC and add the new offset
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second) |> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|> calendar.naive_datetime_from_iso_days() |> calendar.naive_datetime_from_iso_days()
end end
@@ -1321,9 +1296,9 @@ defmodule DateTime do
end end
@doc """ @doc """
Converts a number of Gregorian seconds to a `DateTime` struct. Converts a number of gregorian seconds to a `DateTime` struct.
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
`DateTime.shift_zone/3`. `DateTime.shift_zone/3`.
## Examples ## Examples
@@ -1366,7 +1341,7 @@ defmodule DateTime do
end end
@doc """ @doc """
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds. Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
## Examples ## Examples
@@ -1601,15 +1576,15 @@ defmodule DateTime do
def diff(datetime1, datetime2, unit \\ :second) def diff(datetime1, datetime2, unit \\ :second)
def diff(datetime1, datetime2, :day) do def diff(datetime1, datetime2, :day) do
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000) diff(datetime1, datetime2, :second) |> div(86400)
end end
def diff(datetime1, datetime2, :hour) do def diff(datetime1, datetime2, :hour) do
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000) diff(datetime1, datetime2, :second) |> div(3600)
end end
def diff(datetime1, datetime2, :minute) do def diff(datetime1, datetime2, :minute) do
diff(datetime1, datetime2, :microsecond) |> div(60_000_000) diff(datetime1, datetime2, :second) |> div(60)
end end
def diff( def diff(
@@ -1647,12 +1622,8 @@ defmodule DateTime do
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`, Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from `:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are `t:System.time_unit/0`. It defaults to `:second`. Negative values
all converted to microseconds. Negative values will move backwards will move backwards in time.
in time and the default precision is `:second`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This function relies on a contiguous representation of time, This function relies on a contiguous representation of time,
ignoring timezone changes. For example, if you add one day when there ignoring timezone changes. For example, if you add one day when there
@@ -1772,9 +1743,6 @@ defmodule DateTime do
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This operation is equivalent to shifting the datetime wall clock This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see (in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it on their watch), then applying the time zone offset to convert it
@@ -1843,6 +1811,44 @@ defmodule DateTime do
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t @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(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 def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{ %{
year: year, year: year,
@@ -2053,12 +2059,11 @@ defmodule DateTime do
end end
defp apply_tz_offset(iso_days, offset) do defp apply_tz_offset(iso_days, offset) do
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second) Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
end end
defp from_map(%{} = datetime_map) do defp from_map(%{} = datetime_map) do
%DateTime{ %DateTime{
calendar: datetime_map.calendar,
year: datetime_map.year, year: datetime_map.year,
month: datetime_map.month, month: datetime_map.month,
day: datetime_map.day, day: datetime_map.day,
+41 -55
View File
@@ -72,7 +72,7 @@ defmodule Duration do
However, once again, it is important to remember that shifting a duration is not 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 arithmetic, so you may want to use the functions in this module depending on what
you want to achieve. Compare the results of both examples below: you to achieve. Compare the results of both examples below:
# Adding one month after the other # Adding one month after the other
iex> date = ~D[2016-01-31] iex> date = ~D[2016-01-31]
@@ -129,16 +129,6 @@ defmodule Duration do
second: 0, second: 0,
microsecond: {0, 0} microsecond: {0, 0}
@typedoc """
The microsecond component of a duration.
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
durations may represent negative amounts of time. The precision is
an integer from 0 to 6 holding the number of significant digits,
as in the calendar types.
"""
@type microsecond :: {value :: integer, precision :: 0..6}
@typedoc """ @typedoc """
The duration struct type. The duration struct type.
""" """
@@ -150,7 +140,7 @@ defmodule Duration do
hour: integer, hour: integer,
minute: integer, minute: integer,
second: integer, second: integer,
microsecond: microsecond() microsecond: Calendar.microsecond()
} }
@typedoc """ @typedoc """
@@ -164,7 +154,7 @@ defmodule Duration do
| {:hour, integer} | {:hour, integer}
| {:minute, integer} | {:minute, integer}
| {:second, integer} | {:second, integer}
| {:microsecond, microsecond()} | {:microsecond, Calendar.microsecond()}
@typedoc """ @typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs. The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
@@ -242,7 +232,7 @@ defmodule Duration do
@doc """ @doc """
Adds units of given durations `d1` and `d2`. Adds units of given durations `d1` and `d2`.
Respects the highest microsecond precision of the two. Respects the the highest microsecond precision of the two.
## Examples ## Examples
@@ -253,26 +243,26 @@ defmodule Duration do
""" """
@spec add(t, t) :: t @spec add(t, t) :: t
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do def add(%Duration{} = d1, %Duration{} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1 {m1, p1} = d1.microsecond
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2 {m2, p2} = d2.microsecond
%Duration{ %Duration{
year: y1 + y2, year: d1.year + d2.year,
month: mo1 + mo2, month: d1.month + d2.month,
week: w1 + w2, week: d1.week + d2.week,
day: day1 + day2, day: d1.day + d2.day,
hour: h1 + h2, hour: d1.hour + d2.hour,
minute: mi1 + mi2, minute: d1.minute + d2.minute,
second: s1 + s2, second: d1.second + d2.second,
microsecond: {ms1 + ms2, max(p1, p2)} microsecond: {m1 + m2, max(p1, p2)}
} }
end end
@doc """ @doc """
Subtracts units of given durations `d1` and `d2`. Subtracts units of given durations `d1` and `d2`.
Respects the highest microsecond precision of the two. Respects the the highest microsecond precision of the two.
## Examples ## Examples
@@ -283,19 +273,19 @@ defmodule Duration do
""" """
@spec subtract(t, t) :: t @spec subtract(t, t) :: t
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do def subtract(%Duration{} = d1, %Duration{} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1 {m1, p1} = d1.microsecond
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2 {m2, p2} = d2.microsecond
%Duration{ %Duration{
year: y1 - y2, year: d1.year - d2.year,
month: mo1 - mo2, month: d1.month - d2.month,
week: w1 - w2, week: d1.week - d2.week,
day: day1 - day2, day: d1.day - d2.day,
hour: h1 - h2, hour: d1.hour - d2.hour,
minute: mi1 - mi2, minute: d1.minute - d2.minute,
second: s1 - s2, second: d1.second - d2.second,
microsecond: {ms1 - ms2, max(p1, p2)} microsecond: {m1 - m2, max(p1, p2)}
} }
end end
@@ -312,16 +302,14 @@ defmodule Duration do
""" """
@spec multiply(t, integer) :: t @spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{ %Duration{
year: y * integer, year: duration.year * integer,
month: mo * integer, month: duration.month * integer,
week: w * integer, week: duration.week * integer,
day: d * integer, day: duration.day * integer,
hour: h * integer, hour: duration.hour * integer,
minute: mi * integer, minute: duration.minute * integer,
second: s * integer, second: duration.second * integer,
microsecond: {ms * integer, p} microsecond: {ms * integer, p}
} }
end end
@@ -339,16 +327,14 @@ defmodule Duration do
""" """
@spec negate(t) :: t @spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do def negate(%Duration{microsecond: {ms, p}} = duration) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{ %Duration{
year: -y, year: -duration.year,
month: -mo, month: -duration.month,
week: -w, week: -duration.week,
day: -d, day: -duration.day,
hour: -h, hour: -duration.hour,
minute: -mi, minute: -duration.minute,
second: -s, second: -duration.second,
microsecond: {-ms, p} microsecond: {-ms, p}
} }
end end
+247 -213
View File
@@ -33,9 +33,8 @@ defmodule Calendar.ISO do
you to format datetimes however else you desire. you to format datetimes however else you desire.
Elixir does not support reduced accuracy formats (for example, a date without Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in components other than seconds the day component) nor decimal precisions in the lowest component (such as
(such as `10:01,5`). Fractional seconds are supported and truncated to `10:01:25,5`).
microsecond precision.
#### Examples #### Examples
@@ -82,11 +81,6 @@ defmodule Calendar.ISO do
iex> Calendar.ISO.parse_time("23") iex> Calendar.ISO.parse_time("23")
{:error, :invalid_format} {:error, :invalid_format}
A decimal fraction is accepted on seconds, but not on any other component:
iex> Calendar.ISO.parse_time("10:01:25,5")
{:ok, {10, 1, 25, {500000, 1}}}
### Extensions ### Extensions
The parser and formatter adopt one ISO 8601 extension: extended year notation. The parser and formatter adopt one ISO 8601 extension: extended year notation.
@@ -188,7 +182,7 @@ defmodule Calendar.ISO do
@type day_of_year :: 1..366 @type day_of_year :: 1..366
@type quarter_of_year :: 1..4 @type quarter_of_year :: 1..4
@type year_of_era :: {1..10_000, era} @type year_of_era :: {1..10000, era}
@seconds_per_minute 60 @seconds_per_minute 60
@seconds_per_hour 60 * 60 @seconds_per_hour 60 * 60
@@ -202,28 +196,14 @@ defmodule Calendar.ISO do
@ext_date_sep ?- @ext_date_sep ?-
@ext_time_sep ?: @ext_time_sep ?:
@days_per_nonleap_year 365
@days_per_leap_year 366
# The ISO epoch starts, in this implementation, # The ISO epoch starts, in this implementation,
# with ~D[0000-01-01]. Era "1" starts # with ~D[0000-01-01]. Era "1" starts
# on ~D[0001-01-01] which is 366 days later. # on ~D[0001-01-01] which is 366 days later.
@iso_epoch 366 @iso_epoch 366
# Constants for date calculations using 400-year era cycles.
# The algorithm uses a March-based year where March 1 is day 0.
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
@days_per_year 365
@years_per_era 400
@days_per_era @years_per_era * @days_per_year + 97
@days_per_4_years 4 * @days_per_year
@days_per_100_years 100 * @days_per_year + 24
@march_1_offset 31 + 29
@unix_epoch_days 719_528
# Month calculation constants: in a March-based year, each 5-month
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
@days_per_5_months 153
@months_per_cycle 5
[match_basic_date, match_ext_date, guard_date, read_date] = [match_basic_date, match_ext_date, guard_date, read_date] =
quote do quote do
[ [
@@ -423,13 +403,16 @@ defmodule Calendar.ISO do
@spec parse_date(String.t(), format) :: @spec parse_date(String.t(), format) ::
{:ok, {year, month, day}} {:ok, {year, month, day}}
| {:error, atom} | {:error, atom}
def parse_date("-" <> string, format) when is_format(format), def parse_date(string, format) when is_binary(string) and is_format(format),
do: parse_date_guarded(string, format)
defp parse_date_guarded("-" <> string, format),
do: do_parse_date(string, -1, format) do: do_parse_date(string, -1, format)
def parse_date("+" <> string, format) when is_format(format), defp parse_date_guarded("+" <> string, format),
do: do_parse_date(string, 1, format) do: do_parse_date(string, 1, format)
def parse_date(string, format) when is_binary(string) and is_format(format), defp parse_date_guarded(string, format),
do: do_parse_date(string, 1, format) do: do_parse_date(string, 1, format)
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
@@ -505,13 +488,16 @@ defmodule Calendar.ISO do
@spec parse_naive_datetime(String.t(), format) :: @spec parse_naive_datetime(String.t(), format) ::
{:ok, {year, month, day, hour, minute, second, microsecond}} {:ok, {year, month, day, hour, minute, second, microsecond}}
| {:error, atom} | {:error, atom}
def parse_naive_datetime("-" <> string, format) when is_format(format), def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
do: parse_naive_datetime_guarded(string, format)
defp parse_naive_datetime_guarded("-" <> string, format),
do: do_parse_naive_datetime(string, -1, format) do: do_parse_naive_datetime(string, -1, format)
def parse_naive_datetime("+" <> string, format) when is_format(format), defp parse_naive_datetime_guarded("+" <> string, format),
do: do_parse_naive_datetime(string, 1, format) do: do_parse_naive_datetime(string, 1, format)
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format), defp parse_naive_datetime_guarded(string, format),
do: do_parse_naive_datetime(string, 1, format) do: do_parse_naive_datetime(string, 1, format)
defp do_parse_naive_datetime( defp do_parse_naive_datetime(
@@ -606,13 +592,16 @@ defmodule Calendar.ISO do
@spec parse_utc_datetime(String.t(), format) :: @spec parse_utc_datetime(String.t(), format) ::
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset} {:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
| {:error, atom} | {:error, atom}
def parse_utc_datetime("-" <> string, format) when is_format(format), def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
do: parse_utc_datetime_guarded(string, format)
defp parse_utc_datetime_guarded("-" <> string, format),
do: do_parse_utc_datetime(string, -1, format) do: do_parse_utc_datetime(string, -1, format)
def parse_utc_datetime("+" <> string, format) when is_format(format), defp parse_utc_datetime_guarded("+" <> string, format),
do: do_parse_utc_datetime(string, 1, format) do: do_parse_utc_datetime(string, 1, format)
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format), defp parse_utc_datetime_guarded(string, format),
do: do_parse_utc_datetime(string, 1, format) do: do_parse_utc_datetime(string, 1, format)
defp do_parse_utc_datetime( defp do_parse_utc_datetime(
@@ -663,12 +652,12 @@ defmodule Calendar.ISO do
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0}) day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
{{year, month, day}, {hour, minute, second, _}} = {{year, month, day}, {hour, minute, second, _}} =
case add_time_unit_to_iso_days({0, day_fraction}, -offset, :second) do case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
{0, day_fraction} -> {0, day_fraction} ->
{{year, month, day}, time_from_day_fraction(day_fraction)} {{year, month, day}, time_from_day_fraction(day_fraction)}
{extra_days, day_fraction} -> {extra_days, day_fraction} ->
base_days = valid_date_to_iso_days(year, month, day) base_days = date_to_iso_days(year, month, day)
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)} {date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
end end
@@ -680,73 +669,75 @@ defmodule Calendar.ISO do
end end
@doc """ @doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compatible unit pairs. Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`. See `Duration.from_iso8601/1`.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom} @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, 1, [], 0) parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end end
def parse_duration("+P" <> string) when byte_size(string) > 0 do def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, 1, [], 0) parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end end
def parse_duration("-P" <> string) when byte_size(string) > 0 do def parse_duration("-P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, -1, [], 0) 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 end
def parse_duration(_) do def parse_duration(_) do
{:error, :invalid_duration} {:error, :invalid_duration}
end end
defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc} defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, sign, acc, _min_position) defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
when byte_size(string) > 0 do parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
parse_duration_time(string, sign, acc, 0)
end end
defp parse_duration_date(string, sign, acc, min_position) do defp parse_duration_date(string, acc, allowed) do
with {integer, <<unit, rest::binary>>} <- Integer.parse(string), with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, next_min_position} <- find_date_unit(min_position, unit) do {key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position) parse_duration_date(rest, [{key, integer} | acc], allowed)
else else
_ -> {:error, :invalid_date_component} _ -> {:error, :invalid_date_component}
end end
end end
defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc} defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, sign, acc, min_position) do defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] -> {second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
with {:second, _next_min_position} <- find_time_unit(min_position, ?S), case parse_microsecond(rest) do
{{ms, precision}, "S"} <- parse_microsecond(rest) do {{ms, precision}, "S"} ->
ms = ms =
case string do case string do
"-" <> _ -> "-" <> _ ->
-ms -ms
_ -> _ ->
ms ms
end end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
{:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc}
else
_ -> _ ->
{:error, :invalid_time_component} {:error, :invalid_time_component}
end end
{integer, <<unit, rest::binary>>} -> {integer, <<next, rest::binary>>} ->
case find_time_unit(min_position, unit) do case find_unit(allowed, next) do
{key, next_min_position} -> {key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position) false -> {:error, :invalid_time_component}
false ->
{:error, :invalid_time_component}
end end
_ -> _ ->
@@ -754,17 +745,9 @@ defmodule Calendar.ISO do
end end
end end
# The minimum position is the earliest unit still allowed, or one past the end. defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1} defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2} defp find_unit([], _unit), do: false
defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3}
defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4}
defp find_date_unit(_min_position, _unit), do: false
defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1}
defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2}
defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3}
defp find_time_unit(_min_position, _unit), do: false
@doc """ @doc """
Returns the `t:Calendar.iso_days/0` format of the specified date. Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -801,13 +784,13 @@ defmodule Calendar.ISO do
## Examples ## Examples
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86_400}}) iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
{0, 1, 1, 0, 0, 0, {0, 6}} {0, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86_400}}) iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
{2000, 1, 1, 0, 0, 0, {0, 6}} {2000, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43_200, 86_400}}) iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
{2000, 1, 1, 12, 0, 0, {0, 6}} {2000, 1, 1, 12, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86_400_000_000}}) iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
{-1, 1, 1, 0, 0, 0, {0, 6}} {-1, 1, 1, 0, 0, 0, {0, 6}}
""" """
@@ -895,52 +878,28 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01. # Converts year, month, day to count of days since 0000-01-01.
@doc false @doc false
def date_to_iso_days(year, month, day) do def date_to_iso_days(0, 1, 1) do
ensure_day_in_month!(year, month, day) 0
valid_date_to_iso_days(year, month, day)
end end
defp valid_date_to_iso_days(0, 1, 1), do: 0 def date_to_iso_days(1970, 1, 1) do
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days 719_528
end
defp valid_date_to_iso_days(year, month, day) do def date_to_iso_days(year, month, day) do
y = if month <= 2, do: year - 1, else: year ensure_day_in_month!(year, month, day)
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
year_of_era = y - era * @years_per_era
month_prime = if month > 2, do: month - 3, else: month + 9
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
day_of_era = days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year 1
era * @days_per_era + day_of_era + @march_1_offset
end end
# Converts count of days since 0000-01-01 to {year, month, day} tuple. # Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false @doc false
def date_from_iso_days(days) do def date_from_iso_days(days) do
z = days - @march_1_offset {year, day_of_year} = days_to_year(days)
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era) extra_day = if leap_year?(year), do: 1, else: 0
day_of_era = z - era * @days_per_era {month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
{year, month, day_in_month + 1}
year_of_era =
div(
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
div(day_of_era, @days_per_era - 1),
@days_per_year
)
day_of_year =
day_of_era -
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
year = year_of_era + era * @years_per_era
year = if month <= 2, do: year + 1, else: year
{year, month, day}
end end
defp div_rem(int1, int2) do defp div_rem(int1, int2) do
@@ -954,9 +913,6 @@ defmodule Calendar.ISO do
end end
end end
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
@doc """ @doc """
Returns how many days there are in the given year-month. Returns how many days there are in the given year-month.
@@ -1137,11 +1093,6 @@ defmodule Calendar.ISO do
It is an integer from 1 to 4. It is an integer from 1 to 4.
In the ISO calendar, the quarter is determined solely by the month, so the `year`
and `day` arguments are ignored. Combination of `year`, `month`, and `day` is not
validated as a valid date, unlike in `day_of_year/3`. Use `valid_date?/3` when
full date validation is required.
## Examples ## Examples
iex> Calendar.ISO.quarter_of_year(2016, 1, 31) iex> Calendar.ISO.quarter_of_year(2016, 1, 31)
@@ -1182,9 +1133,9 @@ defmodule Calendar.ISO do
""" """
@doc since: "1.8.0" @doc since: "1.8.0"
@spec year_of_era(year) :: {1..10_000, era} @spec year_of_era(year) :: {1..10000, era}
def year_of_era(year) when is_year_CE(year), do: {year, 1} def year_of_era(year) when is_year_CE(year), do: {year, 1}
def year_of_era(year) when is_year_BCE(year), do: {1 - year, 0} def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
@doc """ @doc """
Calendar callback to compute the year and era from the Calendar callback to compute the year and era from the
@@ -1208,7 +1159,7 @@ defmodule Calendar.ISO do
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@impl true @impl true
@spec year_of_era(year, month, day) :: {1..10_000, era} @spec year_of_era(year, month, day) :: {1..10000, era}
def year_of_era(year, _month, _day), do: year_of_era(year) def year_of_era(year, _month, _day), do: year_of_era(year)
@doc """ @doc """
@@ -1237,7 +1188,7 @@ defmodule Calendar.ISO do
end end
def day_of_era(year, month, day) when is_year_BCE(year) do def day_of_era(year, month, day) when is_year_BCE(year) do
day = @iso_epoch - date_to_iso_days(year, month, day) day = abs(date_to_iso_days(year, month, day) - @iso_epoch)
{day, 0} {day, 0}
end end
@@ -1464,7 +1415,7 @@ defmodule Calendar.ISO do
@doc """ @doc """
Converts the given naive_datetime into a iodata. Converts the given naive_datetime into a iodata.
See `naive_datetime_to_string/8` for more information. See `naive_datetime_to_iodata/8` for more information.
## Examples ## Examples
@@ -1589,7 +1540,7 @@ defmodule Calendar.ISO do
@doc """ @doc """
Converts the given datetime into a iodata. Converts the given datetime into a iodata.
See `datetime_to_string/12` for more information. See `datetime_to_iodata/12` for more information.
## Examples ## Examples
@@ -1693,7 +1644,7 @@ defmodule Calendar.ISO do
end end
@doc """ @doc """
Determines if the time given is valid. Determines if the date given is valid according to the proleptic Gregorian calendar.
Leap seconds are not supported by the built-in Calendar.ISO. Leap seconds are not supported by the built-in Calendar.ISO.
@@ -1713,7 +1664,7 @@ defmodule Calendar.ISO do
boolean boolean
def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond) def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond)
when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and
is_integer(ms_precision) do is_integer(ms_value) do
is_hour(hour) and is_minute(minute) and is_second(second) and is_hour(hour) and is_minute(minute) and is_second(second) and
is_microsecond(ms_value, ms_precision) is_microsecond(ms_value, ms_precision)
end end
@@ -1753,11 +1704,11 @@ defmodule Calendar.ISO do
## Examples ## Examples
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86_400_000_000}}) iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86400000000}})
{0, {0, 86400000000}} {0, {0, 86400000000}}
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {43_200_000_000, 86_400_000_000}}) iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {43200000000, 86400000000}})
{730485, {0, 86400000000}} {730485, {0, 86400000000}}
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {46_800_000_000, 86_400_000_000}}) iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {46800000000, 86400000000}})
{730485, {0, 86400000000}} {730485, {0, 86400000000}}
""" """
@@ -1773,11 +1724,11 @@ defmodule Calendar.ISO do
## Examples ## Examples
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86_400_000_000}}) iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86400000000}})
{0, {86399999999, 86400000000}} {0, {86399999999, 86400000000}}
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {43_200_000_000, 86_400_000_000}}) iex> Calendar.ISO.iso_days_to_end_of_day({730485, {43200000000, 86400000000}})
{730485, {86399999999, 86400000000}} {730485, {86399999999, 86400000000}}
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {46_800_000_000, 86_400_000_000}}) iex> Calendar.ISO.iso_days_to_end_of_day({730485, {46800000000, 86400000000}})
{730485, {86399999999, 86400000000}} {730485, {86399999999, 86400000000}}
""" """
@@ -1846,9 +1797,6 @@ defmodule Calendar.ISO do
shift_options = shift_datetime_options(duration) shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{:microsecond, {0, _}}, naive_datetime ->
naive_datetime
{_, 0}, naive_datetime -> {_, 0}, naive_datetime ->
naive_datetime naive_datetime
@@ -1878,9 +1826,6 @@ defmodule Calendar.ISO do
shift_options = shift_time_options(duration) shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{:microsecond, {0, _}}, time ->
time
{_, 0}, time -> {_, 0}, time ->
time time
@@ -1892,7 +1837,7 @@ defmodule Calendar.ISO do
@doc false @doc false
def shift_days({year, month, day}, days) do def shift_days({year, month, day}, days) do
{year, month, day} = {year, month, day} =
valid_date_to_iso_days(year, month, day) date_to_iso_days(year, month, day)
|> Kernel.+(days) |> Kernel.+(days)
|> date_from_iso_days() |> date_from_iso_days()
@@ -1903,7 +1848,7 @@ defmodule Calendar.ISO do
months_in_year = 12 months_in_year = 12
total_months = year * months_in_year + month + months - 1 total_months = year * months_in_year + month + months - 1
new_year = floor_div_positive_divisor(total_months, months_in_year) new_year = Integer.floor_div(total_months, months_in_year)
new_month = new_month =
case rem(total_months, months_in_year) + 1 do case rem(total_months, months_in_year) + 1 do
@@ -1943,7 +1888,8 @@ defmodule Calendar.ISO do
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit) def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
add_time_unit_to_iso_days(iso_days, value, unit) ppd = System.convert_time_unit(86400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end end
defp shift_time_unit_values({0, _}, {_, original_precision}) do defp shift_time_unit_values({0, _}, {_, original_precision}) do
@@ -1991,7 +1937,7 @@ defmodule Calendar.ISO do
}) do }) do
[ [
month: year * 12 + month, month: year * 12 + month,
second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second, second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond microsecond: microsecond
] ]
end end
@@ -2024,8 +1970,8 @@ defmodule Calendar.ISO do
total = System.convert_time_unit(integer, unit, :microsecond) total = System.convert_time_unit(integer, unit, :microsecond)
if total in @unix_range_microseconds do if total in @unix_range_microseconds do
{seconds, microseconds} = div_rem(total, @microseconds_per_second) microseconds = Integer.mod(total, @microseconds_per_second)
seconds = @unix_epoch + seconds seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
precision = precision_for_unit(unit) precision = precision_for_unit(unit)
{date, time} = iso_seconds_to_datetime(seconds) {date, time} = iso_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}} {:ok, date, time, {microseconds, precision}}
@@ -2046,27 +1992,38 @@ defmodule Calendar.ISO do
end end
end end
defp parse_microsecond("." <> rest), do: parse_microsecond(rest, rest, 0) defp parse_microsecond("." <> rest) do
defp parse_microsecond("," <> rest), do: parse_microsecond(rest, rest, 0) case parse_microsecond(rest, 0, []) do
defp parse_microsecond(rest), do: {{0, 0}, rest} {[], 0, _} ->
:error
# Digits past the sixth are consumed but do not contribute to the value. {microsecond, precision, rest} ->
defp parse_microsecond(<<head, tail::binary>>, digits, 6) when head in ?0..?9, scale = scale_factor(precision)
do: parse_microsecond(tail, digits, 6) {{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
end
end
defp parse_microsecond(<<head, tail::binary>>, digits, precision) when head in ?0..?9, defp parse_microsecond("," <> rest) do
do: parse_microsecond(tail, digits, precision + 1) parse_microsecond("." <> rest)
end
defp parse_microsecond(_rest, _digits, 0), do: :error defp parse_microsecond(rest) do
{{0, 0}, rest}
end
defp parse_microsecond(rest, digits, precision) do defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
scale = scale_factor(precision) do: parse_microsecond(tail, 6, acc)
microsecond = :erlang.binary_to_integer(:binary.part(digits, 0, precision)) * scale
{{microsecond, precision}, rest} defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
do: parse_microsecond(tail, precision + 1, [head | acc])
defp parse_microsecond(rest, precision, acc) do
{:lists.reverse(acc), precision, rest}
end end
defp parse_offset(""), do: {nil, ""} defp parse_offset(""), do: {nil, ""}
defp parse_offset("Z"), do: {0, ""} defp parse_offset("Z"), do: {0, ""}
defp parse_offset("-00:00"), do: :error
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>), defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
do: parse_offset(1, h1, h2, m1, m2, rest) do: parse_offset(1, h1, h2, m1, m2, rest)
@@ -2089,8 +2046,7 @@ defmodule Calendar.ISO do
true <- m1 in ?0..?5 and m2 in ?0..?9, true <- m1 in ?0..?5 and m2 in ?0..?9,
hour = (h1 - ?0) * 10 + h2 - ?0, hour = (h1 - ?0) * 10 + h2 - ?0,
min = (m1 - ?0) * 10 + m2 - ?0, min = (m1 - ?0) * 10 + m2 - ?0,
true <- hour < 24, true <- hour < 24 do
true <- sign == 1 or hour != 0 or min != 0 do
{(hour * 60 + min) * 60 * sign, rest} {(hour * 60 + min) * 60 * sign, rest}
else else
_ -> :error _ -> :error
@@ -2106,43 +2062,10 @@ defmodule Calendar.ISO do
end end
@doc false @doc false
def iso_days_to_unit(iso_days, :second) do def iso_days_to_unit({days, {parts, ppd}}, unit) do
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), @microseconds_per_second) day_microseconds = days * @parts_per_day
end microseconds = divide_by_parts_per_day(parts, ppd)
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
def iso_days_to_unit(iso_days, :millisecond) do
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), 1_000)
end
def iso_days_to_unit(iso_days, :microsecond) do
iso_days_to_microseconds(iso_days)
end
def iso_days_to_unit(iso_days, unit) do
System.convert_time_unit(iso_days_to_microseconds(iso_days), :microsecond, unit)
end
defp iso_days_to_microseconds({days, {parts, ppd}}) do
days * @parts_per_day + divide_by_parts_per_day(parts, ppd)
end
@doc false
def add_time_unit_to_iso_days(iso_days, add, :second) do
add_day_fraction_to_iso_days(iso_days, add * @microseconds_per_second, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, :millisecond) do
add_day_fraction_to_iso_days(iso_days, add * 1_000, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, :microsecond) do
add_day_fraction_to_iso_days(iso_days, add, @parts_per_day)
end
def add_time_unit_to_iso_days(iso_days, add, unit)
when unit == :nanosecond or is_integer(unit) do
ppd = System.convert_time_unit(@seconds_per_day, :second, unit)
add_day_fraction_to_iso_days(iso_days, add, ppd)
end end
@doc false @doc false
@@ -2170,12 +2093,9 @@ defmodule Calendar.ISO do
end end
end end
defp leap_day_offset(_year, month) when month < 3, do: 0 # Note that this function does not add the extra leap day for a leap year.
# If you want to add that leap day when appropriate,
defp leap_day_offset(year, _month) do # add the result of leap_day_offset/2 to the result of days_before_month/1.
if leap_year?(year), do: 1, else: 0
end
defp days_before_month(1), do: 0 defp days_before_month(1), do: 0
defp days_before_month(2), do: 31 defp days_before_month(2), do: 31
defp days_before_month(3), do: 59 defp days_before_month(3), do: 59
@@ -2189,6 +2109,120 @@ defmodule Calendar.ISO do
defp days_before_month(11), do: 304 defp days_before_month(11), do: 304
defp days_before_month(12), do: 334 defp days_before_month(12), do: 334
defp leap_day_offset(_year, month) when month < 3, do: 0
defp leap_day_offset(year, _month) do
if leap_year?(year), do: 1, else: 0
end
defp days_to_year(days) when days < 0 do
year_estimate = -div(-days, @days_per_nonleap_year) - 1
{year, days_before_year} =
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
leap_year_pad = if leap_year?(year), do: 1, else: 0
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
end
defp days_to_year(days) do
year_estimate = div(days, @days_per_nonleap_year)
{year, days_before_year} =
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
{year, days - days_before_year}
end
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
end
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
end
defp days_to_year(year, _days1, days2) do
{year, days2}
end
defp days_to_end_of_epoch(year) when year < 0 do
previous_year = year + 1
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
previous_year * @days_per_nonleap_year
end
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
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 +
@days_per_leap_year
end
# Note that 0 is the first day of the month.
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
{1, day_of_year}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
{2, day_of_year - 31}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
{3, day_of_year - (59 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
{4, day_of_year - (90 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
{5, day_of_year - (120 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
{6, day_of_year - (151 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
{7, day_of_year - (181 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
{8, day_of_year - (212 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
{9, day_of_year - (243 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
{10, day_of_year - (273 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
{11, day_of_year - (304 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) do
{12, day_of_year - (334 + extra_day)}
end
defp iso_seconds_to_datetime(seconds) do defp iso_seconds_to_datetime(seconds) do
{days, rest_seconds} = div_rem(seconds, @seconds_per_day) {days, rest_seconds} = div_rem(seconds, @seconds_per_day)
+40 -49
View File
@@ -164,7 +164,7 @@ defmodule NaiveDateTime do
Returns the "local time" for the machine the Elixir program is running on. Returns the "local time" for the machine the Elixir program is running on.
WARNING: This function can cause insidious bugs. It depends on the time zone WARNING: This function can cause insidious bugs. It depends on the time zone
configuration at run time. This can change and be set to a time zone that has configuration at run time. This can changed and be set to a time zone that has
daylight saving jumps (spring forward or fall back). daylight saving jumps (spring forward or fall back).
This function can be used to display what the time is right now for the time This function can be used to display what the time is right now for the time
@@ -402,9 +402,8 @@ defmodule NaiveDateTime do
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`, Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from `:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are `t:System.time_unit/0`. It defaults to `:second`. Negative values
all converted to microseconds. Negative values will move backwards will move backwards in time.
in time and the default precision is `:second`.
## Examples ## Examples
@@ -541,15 +540,15 @@ defmodule NaiveDateTime do
def diff(naive_datetime1, naive_datetime2, unit \\ :second) def diff(naive_datetime1, naive_datetime2, unit \\ :second)
def diff(naive_datetime1, naive_datetime2, :day) do def diff(naive_datetime1, naive_datetime2, :day) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
end end
def diff(naive_datetime1, naive_datetime2, :hour) do def diff(naive_datetime1, naive_datetime2, :hour) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
end end
def diff(naive_datetime1, naive_datetime2, :minute) do def diff(naive_datetime1, naive_datetime2, :minute) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(60)
end end
def diff( def diff(
@@ -570,11 +569,9 @@ defmodule NaiveDateTime do
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" "unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end end
diff_microsecond = units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) - units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) units1 - units2
System.convert_time_unit(diff_microsecond, :microsecond, unit)
end end
@doc """ @doc """
@@ -674,7 +671,7 @@ defmodule NaiveDateTime do
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t() @spec truncate(t(), :microsecond | :millisecond | :second) :: t()
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)} %{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
end end
@@ -717,18 +714,16 @@ defmodule NaiveDateTime do
""" """
@spec to_date(Calendar.naive_datetime()) :: Date.t() @spec to_date(Calendar.naive_datetime()) :: Date.t()
def to_date( def to_date(%{
%{ year: year,
year: year, month: month,
month: month, day: day,
day: day, calendar: calendar,
calendar: calendar, hour: _,
hour: _, minute: _,
minute: _, second: _,
second: _, microsecond: _
microsecond: _ }) do
} = _naive_datetime
) do
%Date{year: year, month: month, day: day, calendar: calendar} %Date{year: year, month: month, day: day, calendar: calendar}
end end
@@ -745,18 +740,16 @@ defmodule NaiveDateTime do
""" """
@spec to_time(Calendar.naive_datetime()) :: Time.t() @spec to_time(Calendar.naive_datetime()) :: Time.t()
def to_time( def to_time(%{
%{ year: _,
year: _, month: _,
month: _, day: _,
day: _, calendar: calendar,
calendar: calendar, hour: hour,
hour: hour, minute: minute,
minute: minute, second: second,
second: second, microsecond: microsecond
microsecond: microsecond }) do
} = _naive_datetime
) do
%Time{ %Time{
hour: hour, hour: hour,
minute: minute, minute: minute,
@@ -1152,18 +1145,16 @@ defmodule NaiveDateTime do
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()} @spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds( def to_gregorian_seconds(%{
%{ calendar: calendar,
calendar: calendar, year: year,
year: year, month: month,
month: month, day: day,
day: day, hour: hour,
hour: hour, minute: minute,
minute: minute, second: second,
second: second, microsecond: {microsecond, precision}
microsecond: {microsecond, precision} }) do
} = _naive_datetime
) do
{days, day_fraction} = {days, day_fraction} =
calendar.naive_datetime_to_iso_days( calendar.naive_datetime_to_iso_days(
year, year,
+20 -44
View File
@@ -62,7 +62,7 @@ defmodule Time do
You can pass a time unit to automatically truncate the resulting time. You can pass a time unit to automatically truncate the resulting time.
The default unit if none gets passed is `:native` which results in a default resolution of microseconds. The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
## Examples ## Examples
@@ -146,9 +146,8 @@ defmodule Time do
iex> Time.new(23, 59, 59, 1_000_000) iex> Time.new(23, 59, 59, 1_000_000)
{:error, :invalid_time} {:error, :invalid_time}
Invalid precision: # Invalid precision
Time.new(23, 59, 59, {999_999, 10})
iex> Time.new(23, 59, 59, {999_999, 10})
{:error, :invalid_time} {:error, :invalid_time}
""" """
@@ -467,12 +466,8 @@ defmodule Time do
Calendar.microsecond(), Calendar.microsecond(),
Calendar.calendar() Calendar.calendar()
) :: t ) :: t
def from_seconds_after_midnight( def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
seconds, when is_integer(seconds) do
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
seconds_in_day = Integer.mod(seconds, @seconds_per_day) seconds_in_day = Integer.mod(seconds, @seconds_per_day)
{hour, minute, second, {_, _}} = {hour, minute, second, {_, _}} =
@@ -483,7 +478,7 @@ defmodule Time do
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, second: second,
microsecond: {microsecond, precision} microsecond: microsecond
} }
end end
@@ -501,7 +496,7 @@ defmodule Time do
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()} @spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
iso_days = {0, to_day_fraction(time)} iso_days = {0, to_day_fraction(time)}
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond} {Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
@@ -519,9 +514,8 @@ defmodule Time do
Accepts an `amount_to_add` in any `unit`. `unit` can be Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from `:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are `t:System.time_unit/0`. It defaults to `:second`. Negative values
all converted to microseconds. Negative values will move backwards will move backwards in time.
in time and the default precision is `:second`.
Note the result value represents the time of day, meaning that it is cyclic, Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar. for instance, it will never go over 24 hours for the ISO calendar.
@@ -694,7 +688,7 @@ defmodule Time do
@doc """ @doc """
Compares two time structs. Compares two time structs.
Returns `:gt` if the first time is later than the second Returns `:gt` if first time is later than the second
and `:lt` for vice versa. If the two times are equal and `:lt` for vice versa. If the two times are equal
`:eq` is returned. `:eq` is returned.
@@ -720,32 +714,14 @@ defmodule Time do
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt @spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
def compare( def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
%{ %{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
hour: hour1, %{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
minute: minute1,
second: second1, case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
microsecond: {microsecond1, _}, {first, second} when first > second -> :gt
calendar: calendar {first, second} when first < second -> :lt
}, _ -> :eq
%{
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, _},
calendar: calendar
}
) do
cond do
hour1 > hour2 -> :gt
hour1 < hour2 -> :lt
minute1 > minute2 -> :gt
minute1 < minute2 -> :lt
second1 > second2 -> :gt
second1 < second2 -> :lt
microsecond1 > microsecond2 -> :gt
microsecond1 < microsecond2 -> :lt
true -> :eq
end end
end end
@@ -924,11 +900,11 @@ defmodule Time do
def diff(time1, time2, unit \\ :second) def diff(time1, time2, unit \\ :second)
def diff(time1, time2, :hour) do def diff(time1, time2, :hour) do
diff(time1, time2, :microsecond) |> div(3_600_000_000) diff(time1, time2, :second) |> div(3600)
end end
def diff(time1, time2, :minute) do def diff(time1, time2, :minute) do
diff(time1, time2, :microsecond) |> div(60_000_000) diff(time1, time2, :second) |> div(60)
end end
def diff( def diff(
+90 -161
View File
@@ -50,7 +50,7 @@ defmodule Code do
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
`ensure_loaded!/1`) to check if a module is loaded before using it and `ensure_loaded!/1`) to check if a module is loaded before using it and
act accordingly. act.
## `ensure_compiled/1` and `ensure_compiled!/1` ## `ensure_compiled/1` and `ensure_compiled!/1`
@@ -258,7 +258,6 @@ defmodule Code do
| {:locals_without_parens, keyword()} | {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()} | {:force_do_end_blocks, boolean()}
| {:migrate, boolean()} | {:migrate, boolean()}
| {:migrate_atom_interpolations, boolean()}
| {:migrate_bitstring_modifiers, boolean()} | {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()} | {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()} | {:migrate_charlists_as_sigils, boolean()}
@@ -273,7 +272,6 @@ defmodule Code do
| {:escape, boolean()} | {:escape, boolean()}
| {:locals_without_parens, keyword()} | {:locals_without_parens, keyword()}
| {:comments, [term()]} | {:comments, [term()]}
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
@typedoc """ @typedoc """
Options for parsing functions that convert strings to quoted expressions. Options for parsing functions that convert strings to quoted expressions.
@@ -287,26 +285,20 @@ defmodule Code do
unescape: boolean(), unescape: boolean(),
existing_atoms_only: boolean(), existing_atoms_only: boolean(),
token_metadata: boolean(), token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}), literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}), static_atoms_encoder: (atom() -> term()),
emit_warnings: boolean() emit_warnings: boolean()
] ]
@typedoc """ @typedoc """
Options for evaluation environment, accepted by `env_for_eval/1`. Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
""" """
@type env_eval_opt :: @type env_eval_opts :: [
{:file, binary()} file: binary(),
| {:line, pos_integer()} line: pos_integer(),
| {:module, module()} module: module(),
prune_binding: boolean()
@typedoc """ ]
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
and `eval_quoted_with_env/4`.
"""
@type eval_opt ::
{:prune_binding, boolean()}
| {:dbg_callback, {module(), atom(), list()}}
@boolean_compiler_options [ @boolean_compiler_options [
:docs, :docs,
@@ -316,16 +308,11 @@ defmodule Code do
:relative_paths :relative_paths
] ]
@list_compiler_options [:tracers, :parser_options, :erlc_options] @list_compiler_options [:tracers, :parser_options]
@available_compiler_options @boolean_compiler_options ++ @available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++ @list_compiler_options ++
[ [:on_undefined_variable, :infer_signatures, :no_warn_undefined]
:on_undefined_variable,
:infer_signatures,
:no_warn_undefined,
:module_definition
]
@doc """ @doc """
Lists all required files. Lists all required files.
@@ -415,7 +402,7 @@ defmodule Code do
operations. operations.
""" """
@spec append_path(Path.t(), cache: boolean()) :: boolean() @spec append_path(Path.t(), cache: boolean()) :: true | false
def append_path(path, opts \\ []) do def append_path(path, opts \\ []) do
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
end end
@@ -554,7 +541,8 @@ defmodule Code do
This is the list of directories the Erlang VM uses for finding This is the list of directories the Erlang VM uses for finding
module code. The list of files is managed per Erlang VM node. module code. The list of files is managed per Erlang VM node.
All paths are expanded with `Path.expand/1` before being deleted. The path is expanded with `Path.expand/1` before being deleted. If the
path does not exist, this function returns `false`.
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec delete_paths([Path.t()]) :: :ok @spec delete_paths([Path.t()]) :: :ok
@@ -580,11 +568,9 @@ defmodule Code do
## Options ## Options
It accepts the same options as both `env_for_eval/1` and It accepts the same options as `env_for_eval/1`. Additionally, you may
`eval_quoted_with_env/4`. Additionally, you may also pass an environment also pass an environment as second argument, so the evaluation happens
as third argument, so the evaluation happens within that environment. within that environment.
## Return
Returns a tuple of the form `{value, binding}`, where `value` is the value Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating returned from evaluating `string`. If an error occurs while evaluating
@@ -614,11 +600,11 @@ defmodule Code do
iex> Enum.sort(binding) iex> Enum.sort(binding)
[a: 3, b: 2] [a: 3, b: 2]
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and For convenience, you can pass `__ENV__/0` as the `opts` argument and
all imports, requires and aliases defined in the current environment all imports, requires and aliases defined in the current environment
will be automatically carried over: will be automatically carried over:
iex> require Integer, warn: false iex> require Integer
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__) iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
iex> result iex> result
3 3
@@ -626,28 +612,21 @@ defmodule Code do
[a: 1, b: 2] [a: 1, b: 2]
""" """
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) :: @spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
{term, binding} def eval_string(string, binding \\ [], opts \\ [])
def eval_string(string, binding \\ [], opts_or_env \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do def eval_string(string, binding, %Macro.Env{} = env) do
validated_eval_string(string, validate_binding(binding), env_for_eval(env), []) validated_eval_string(string, binding, env)
end end
def eval_string(string, binding, opts) when is_list(opts) do def eval_string(string, binding, opts) when is_list(opts) do
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts) validated_eval_string(string, binding, opts)
end end
defp validate_binding(binding) when is_list(binding), do: binding defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
defp validate_binding(binding) do
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
end
defp validated_eval_string(string, binding, env, opts) do
%{line: line, file: file} = env
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, []) forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts]) {value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding} {value, binding}
end end
@@ -768,12 +747,6 @@ defmodule Code do
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options * `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`. to `true` by default. Defaults to `false`.
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`, * `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
removes unnecessary parentheses in known bitstring removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>` [modifiers](`<<>>/1`), for example `<<foo::binary()>>`
@@ -1156,8 +1129,7 @@ defmodule Code do
returned quoted expressions (instead of evaluated). returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types. See `eval_string/3` for a description of arguments and return types.
It accepts the same options as both `env_for_eval/1` and The options are described under `env_for_eval/1`.
`eval_quoted_with_env/4`.
## Examples ## Examples
@@ -1179,20 +1151,11 @@ defmodule Code do
[a: 1, b: 2] [a: 1, b: 2]
""" """
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) :: @spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
{term, binding} def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) {value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
end
def eval_quoted(quoted, binding, opts) when is_list(opts) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
end
defp eval_quoted(quoted, binding, env, opts) do
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
{value, binding} {value, binding}
end end
@@ -1220,9 +1183,14 @@ defmodule Code do
* `:module` - the module to run the environment on * `:module` - the module to run the environment on
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t() @spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts) def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """ @doc """
@@ -1236,19 +1204,11 @@ defmodule Code do
## Options ## Options
* `:prune_binding` - (since v1.14.2) prune binding to keep only It accepts the same options as `env_for_eval/1`.
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
used in the evaluated code. It must be a `{module, function, args}`
tuple, see `dbg/2` for more details.
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) :: @spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
{term, binding, Macro.Env.t()} {term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ []) def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do when is_list(binding) do
@@ -1298,9 +1258,9 @@ defmodule Code do
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST. * `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
It must be a function that receives two arguments, the literal and its It must be a function that receives two arguments, the literal and its
metadata, and it must return `{:ok, ast :: Macro.t}` or metadata, and it must return `{:ok, ast :: Macro.t}` or
`{:error, reason :: binary}`. If you return anything other than the literal `{:error, reason :: binary}`. If you return anything than the literal
itself as the `term`, then the AST is no longer valid. This option itself as the `term`, then the AST is no longer valid. This option
may still be useful for textual analysis of the source code. may still useful for textual analysis of the source code.
* `:static_atoms_encoder` - the static atom encoder function, see * `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this "The `:static_atoms_encoder` function" section below. Note this
@@ -1326,7 +1286,7 @@ defmodule Code do
and keyword lists. and keyword lists.
The encoder function will receive the atom name (as a binary) and a The encoder function will receive the atom name (as a binary) and a
keyword list with the current line and column. It must return keyword list with the current file, line and column. It must return
`{:ok, token :: term} | {:error, reason :: binary}`. `{:ok, token :: term} | {:error, reason :: binary}`.
The encoder function is supposed to create an atom from the given The encoder function is supposed to create an atom from the given
@@ -1365,7 +1325,14 @@ defmodule Code do
file = Keyword.get(opts, :file, "nofile") file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1) line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1) column = Keyword.get(opts, :column, 1)
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, _error_msg} = error ->
error
end
end end
@doc """ @doc """
@@ -1394,15 +1361,12 @@ defmodule Code do
while preserving information like comments and literals position. while preserving information like comments and literals position.
Returns `{:ok, quoted_form, comments}` if it succeeds, Returns `{:ok, quoted_form, comments}` if it succeeds,
`{:error, {location, error, token}}` otherwise, where `location` `{:error, {line, error, token}}` otherwise.
is keyword metadata containing the line and column of the error.
Comments are maps with the following fields: Comments are maps with the following fields:
* `:line` - The line number of the source code * `:line` - The line number of the source code
* `:column` - The column number of the source code
* `:text` - The full text of the comment, including the leading `#` * `:text` - The full text of the comment, including the leading `#`
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment * `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
@@ -1444,7 +1408,8 @@ defmodule Code do
Process.put(:code_formatter_comments, []) Process.put(:code_formatter_comments, [])
opts = [preserve_comments: &preserve_comments/5] ++ opts opts = [preserve_comments: &preserve_comments/5] ++ opts
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
comments = Enum.reverse(Process.get(:code_formatter_comments)) comments = Enum.reverse(Process.get(:code_formatter_comments))
{:ok, forms, comments} {:ok, forms, comments}
end end
@@ -1457,9 +1422,7 @@ defmodule Code do
Returns the AST and a list of comments if it succeeds, raises an exception Returns the AST and a list of comments if it succeeds, raises an exception
otherwise. The exception is a `TokenMissingError` in case a token is missing otherwise. The exception is a `TokenMissingError` in case a token is missing
(usually because the expression is incomplete), `MismatchedDelimiterError` (usually because the expression is incomplete), `SyntaxError` otherwise.
(in case of mismatched opening and closing delimiters) and `SyntaxError`
otherwise.
Check `string_to_quoted/2` for options information. Check `string_to_quoted/2` for options information.
""" """
@@ -1562,13 +1525,9 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from `string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`. escaping the sequences twice. Defaults to `true`.
* `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information.
See `format_string!/2` for the full list of formatting options including See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all `:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
migration options like `:migrate_charlists_as_sigils`. Note `:line_length` `:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
does not apply here.
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) :: @spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
@@ -1650,19 +1609,13 @@ defmodule Code do
nil nil
:proceed -> :proceed ->
try do loaded =
loaded = Module.ParallelChecker.verify(fn ->
Module.ParallelChecker.verify(fn -> :elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end) end)
end)
:elixir_code_server.cast({:required, file}) :elixir_code_server.cast({:required, file})
loaded loaded
catch
kind, reason ->
:elixir_code_server.call({:release, file})
:erlang.raise(kind, reason, __STACKTRACE__)
end
end end
end end
@@ -1688,7 +1641,7 @@ defmodule Code do
@doc """ @doc """
Stores all given compilation options. Stores all given compilation options.
Changing the compilation options affects all processes Changing the compilation options affect all processes
running in a given Erlang VM node. To store individual running in a given Erlang VM node. To store individual
options and for a description of all options, see options and for a description of all options, see
`put_compiler_option/2`. `put_compiler_option/2`.
@@ -1752,11 +1705,14 @@ defmodule Code do
@doc """ @doc """
Stores a compilation option. Stores a compilation option.
Changing the compilation options affects all processes running in a Changing the compilation options affect all processes running in a
given Erlang VM node. given Erlang VM node.
Available options are: Available options are:
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled * `:debug_info` - when `true`, retains debug information in the compiled
module. This option can also be overridden per module using the `@compile` module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`. directive. Defaults to `true`.
@@ -1768,17 +1724,10 @@ defmodule Code do
remove the `:debug_info` while deploying, tools like `mix release` remove the `:debug_info` while deploying, tools like `mix release`
already do such by default. already do such by default.
Other environments, such as `mix test`, automatically disable this Other environments, such as `mix test`, automatically disables this
via the `:test_elixirc_options` project configuration, as there is via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files. typically no need to store debug chunks for test files.
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
debug metadata for BEAM debuggers. Defaults to `[]`.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn * `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added. when a protocol has already been consolidated and a new implementation is added.
Defaults to `false`. Defaults to `false`.
@@ -1786,41 +1735,28 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has * `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`. already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules * `:infer_signatures` (since v1.18.0) - a list of applications of which modules
should be used during type inference. When `false`, it disables module-local should be using during type inference. When `false`, it disables module-local
signature inference used when type checking remote calls to the compiled signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of the value of this option. module. Type checking will be executed regardless of the value of this option.
Mix projects will set this option to your dependencies list in dev/prod, and Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
it will disable this option during test (as there is typically no need to infer
signatures for test files). Outside of Mix projects, it defaults to `[:elixir]`.
* `:module_definition` (since v1.20.0) - stores if the module definition should When setting this option, we recommend running `mix clean` so the current module
be `:compiled` (the default) or `:interpreted`. Note this does not affect the may be compiled from scratch. `mix test` automatically disables this option via
`.beam` file written to disk, only how the contents inside `defmodule` are the `:test_elixirc_options` project configuration, as there is typically no need
executed. Using the `:interpreted` mode may offer better compilation times for to infer signatures for test files.
large projects, especially on machines with high core count, however, it comes
with some downsides:
* Errors during compilation may have less precise stacktraces * `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
* Anonymous functions within `defmodule` can have only up to 20 arguments. won't affect runtime warnings and errors. Defaults to `true`.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}` * `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
tuples that will not emit warnings that the module or function does not exist tuples that will not emit warnings that the module or function does not exist
at compilation time. Pass atom `:all` to skip warning for all undefined at compilation time. Pass atom `:all` to skip warning for all undefined
functions. This can be useful when doing dynamic compilation. Defaults to `[]`. functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`. * `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
When `:raise` (the default), undefined variables will trigger a compilation compilation. See the module docs for more information. Defaults to `[]`.
error. You may 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.
* `:parser_options` (since v1.10.0) - a keyword list of options to be given * `:parser_options` (since v1.10.0) - a keyword list of options to be given
to the parser when compiling files. It accepts the same options as to the parser when compiling files. It accepts the same options as
@@ -1831,12 +1767,14 @@ defmodule Code do
and `compile_file/2` but not `string_to_quoted/2` and friends, as the and `compile_file/2` but not `string_to_quoted/2` and friends, as the
latter is used for other purposes beyond compilation. latter is used for other purposes beyond compilation.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes, * `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
warnings, and errors generated by the compiler. Note disabling this option When `:raise` (the default), undefined variables will trigger a compilation
won't affect runtime warnings and errors. Defaults to `true`. 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
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during of the same name (for example, `node` would be expanded as `node()`).
compilation. See the module docs for more information. Defaults to `[]`. 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.
It always returns `:ok`. Raises an error for invalid options. It always returns `:ok`. Raises an error for invalid options.
@@ -1876,15 +1814,6 @@ defmodule Code do
:ok :ok
end end
def put_compiler_option(:module_definition, value) do
if value not in [:interpreted, :compiled] do
raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}"
end
:elixir_config.put(:module_definition, value)
:ok
end
def put_compiler_option(:infer_signatures, value) do def put_compiler_option(:infer_signatures, value) do
value = value =
cond do cond do
@@ -2150,7 +2079,7 @@ defmodule Code do
If the module being checked is currently in a compiler deadlock, If the module being checked is currently in a compiler deadlock,
this function returns `{:error, :unavailable}`. Unavailable doesn't this function returns `{:error, :unavailable}`. Unavailable doesn't
necessarily mean the module doesn't exist, just that it is not currently necessarily mean the module doesn't exist, just that it is not currently
available, but it may (or may not) become available in the future. available, but it (or may not) become available in the future.
Therefore, if you can only continue if the module is available, use Therefore, if you can only continue if the module is available, use
`ensure_compiled!/1` instead. In particular, do not do this: `ensure_compiled!/1` instead. In particular, do not do this:
+10 -18
View File
@@ -195,7 +195,6 @@ defmodule Code.Formatter do
file = Keyword.get(opts, :file, nil) file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, []) sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false) migrate = Keyword.get(opts, :migrate, false)
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate) migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate) migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate) migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
@@ -224,7 +223,6 @@ defmodule Code.Formatter do
comments: comments, comments: comments,
sigils: sigils, sigils: sigils,
file: file, file: file,
migrate_atom_interpolations: migrate_atom_interpolations,
migrate_bitstring_modifiers: migrate_bitstring_modifiers, migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe, migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
migrate_charlists_as_sigils: migrate_charlists_as_sigils, migrate_charlists_as_sigils: migrate_charlists_as_sigils,
@@ -336,20 +334,14 @@ defmodule Code.Formatter do
end end
defp quoted_to_algebra( defp quoted_to_algebra(
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} = {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
quoted,
context, context,
state state
) do ) do
cond do if interpolated?(entries) do
not interpolated?(entries) -> interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
remote_to_algebra(quoted, context, state) else
remote_to_algebra(quoted, context, state)
state.migrate_atom_interpolations ->
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
true ->
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
end end
end end
@@ -1465,7 +1457,7 @@ defmodule Code.Formatter do
metadata = [ metadata = [
file: state.file, file: state.file,
line: meta[:line], line: meta[:line],
sigil: String.to_unsafe_atom(name), sigil: String.to_atom(name),
modifiers: modifiers, modifiers: modifiers,
opening_delimiter: opening_delimiter opening_delimiter: opening_delimiter
] ]
@@ -1704,7 +1696,7 @@ defmodule Code.Formatter do
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts) iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
end end
defp integer_to_algebra(text, inspect_opts) do defp integer_to_algebra(text, inspect_otps) do
case text do case text do
<<?0, ?x, rest::binary>> -> <<?0, ?x, rest::binary>> ->
"0x" <> String.upcase(rest) "0x" <> String.upcase(rest)
@@ -1718,15 +1710,15 @@ defmodule Code.Formatter do
decimal -> decimal ->
insert_underscores(decimal) insert_underscores(decimal)
end end
|> color_doc(:number, inspect_opts) |> color_doc(:number, inspect_otps)
end end
defp float_to_algebra(text, inspect_opts) do defp float_to_algebra(text, inspect_otps) do
[int_part, decimal_part] = :binary.split(text, ".") [int_part, decimal_part] = :binary.split(text, ".")
decimal_part = String.downcase(decimal_part) decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_opts) color_doc(string, :number, inspect_otps)
end end
defp insert_underscores("-" <> digits) do defp insert_underscores("-" <> digits) do
+11 -51
View File
@@ -27,8 +27,7 @@ defmodule Code.Fragment do
column: pos_integer(), column: pos_integer(),
columns: boolean(), columns: boolean(),
token_metadata: boolean(), token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}), literal_encoder: (term(), Macro.metadata() -> term()),
preserve_sigils: boolean(),
trailing_fragment: String.t() trailing_fragment: String.t()
] ]
@@ -36,7 +35,7 @@ defmodule Code.Fragment do
Returns the list of lines in the given string, preserving their line endings. Returns the list of lines in the given string, preserving their line endings.
Only the line endings recognized by the Elixir compiler are Only the line endings recognized by the Elixir compiler are
considered, namely `\r\n` and `\n`. If you would like to retrieve considered, namely `\r\n` and `\n`. If you would like the retrieve
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`. lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
## Examples ## Examples
@@ -52,7 +51,6 @@ defmodule Code.Fragment do
""" """
@doc since: "1.19.0" @doc since: "1.19.0"
@spec lines(String.t()) :: [String.t()]
def lines(string) do def lines(string) do
lines(string, <<>>) lines(string, <<>>)
end end
@@ -142,9 +140,6 @@ defmodule Code.Fragment do
* `{:anonymous_call, inside_caller}` - the context is an anonymous * `{:anonymous_call, inside_caller}` - the context is an anonymous
call, such as `fun.(` and `@fun.(`. call, such as `fun.(` and `@fun.(`.
* `{:capture_arg, charlist}` - the context is a capture argument,
such as `&1`
* `{:module_attribute, charlist}` - the context is a module attribute, * `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor` such as `@hello_wor`
@@ -162,8 +157,8 @@ defmodule Code.Fragment do
* `:none` - no context possible * `:none` - no context possible
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning * `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
`~>` and `~>>`, are returned as :operator contexts `~>` and `~>>`
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`. * `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
`inside_struct` can either be a `charlist` in case of a static alias or an `inside_struct` can either be a `charlist` in case of a static alias or an
@@ -209,7 +204,6 @@ defmodule Code.Fragment do
| {:local_arity, charlist} | {:local_arity, charlist}
| {:local_call, charlist} | {:local_call, charlist}
| {:anonymous_call, inside_caller} | {:anonymous_call, inside_caller}
| {:capture_arg, charlist}
| {:module_attribute, charlist} | {:module_attribute, charlist}
| {:operator, charlist} | {:operator, charlist}
| {:operator_arity, charlist} | {:operator_arity, charlist}
@@ -662,7 +656,7 @@ defmodule Code.Fragment do
iex> Code.Fragment.surround_context("foo", {1, 1}) 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, ~c"foo"}, end: {1, 4}}
## Differences from `cursor_context/2` ## Differences to `cursor_context/2`
Because `surround_context/3` attempts to capture complex expressions, Because `surround_context/3` attempts to capture complex expressions,
it has some differences to `cursor_context/2`: it has some differences to `cursor_context/2`:
@@ -676,7 +670,7 @@ defmodule Code.Fragment do
be a local or variable be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}` * `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`) (in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs * This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context `{:struct, ~c""}` as context
@@ -1240,43 +1234,14 @@ defmodule Code.Fragment do
the cursor. This is necessary to correctly complete anonymous functions the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->` and the left-hand side of `->`
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
(see "Tracking sigils" section below)
## Tracking sigils
The `:preserve_sigils` option can be used to track cursor positions inside
a sigil.
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
as the second argument:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
In case the sigil is completed and has zero or more modifiers, the cursor will
be nested in the list, with all previous delimiters specified:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
If the cursor is after the sigil, then it is discarded as everything else:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
{:ok, {:__cursor__, [line: 1], []}}
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) :: @spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}} {:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment) {trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder]) opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
file = Keyword.get(opts, :file, "nofile") file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1) line = Keyword.get(opts, :line, 1)
@@ -1296,10 +1261,7 @@ defmodule Code.Fragment do
end end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators) tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:ok, line, column, _warnings, rev_tokens, rev_terminators} -> {:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens = tokens =
@@ -1307,7 +1269,7 @@ defmodule Code.Fragment do
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])), Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true), true <- maybe_missing_stab?(rev_tokens, true),
opts = opts =
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}), Keyword.put(opts, :check_terminators, {:cursor, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <- {:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do :elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens = trailing_tokens =
@@ -1326,12 +1288,10 @@ defmodule Code.Fragment do
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators) _ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end end
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do :elixir.tokens_to_quoted(tokens, file, opts)
{:ok, forms}
end
{:error, info, _rest, _warnings, _so_far} -> {:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir_tokenizer.format_error(info)} {:error, :elixir.format_token_error(info)}
end end
end end
+4 -4
View File
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
with "-" <> rest <- Atom.to_string(atom), with "-" <> rest <- Atom.to_string(atom),
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(), [trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
[arity, _inner, _count, ""] <- String.split(trailing, "-") do [arity, _inner, _count, ""] <- String.split(trailing, "-") do
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity} {reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
else else
_ -> :error _ -> :error
end end
@@ -150,20 +150,20 @@ defmodule Code.Identifier do
<<acc::binary, char::utf8>> <<acc::binary, char::utf8>>
end end
defp escape_char(char, acc) when char < 0x80 do defp escape_char(char, acc) when char < 0x100 do
<<a::4, b::4>> = <<char::8>> <<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>> <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
end end
defp escape_char(char, acc) when char < 0x10000 do defp escape_char(char, acc) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>> <<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>> <<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
end end
defp escape_char(char, acc) when char < 0x1000000 do defp escape_char(char, acc) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>> <<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), <<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>> ?}>>
end end
+14 -37
View File
@@ -4,8 +4,6 @@
defmodule Code.Normalizer do defmodule Code.Normalizer do
@moduledoc false @moduledoc false
@do_end_keywords [:rescue, :catch, :else, :after]
defguard is_literal(x) defguard is_literal(x)
when is_integer(x) or when is_integer(x) or
is_float(x) or is_float(x) or
@@ -70,7 +68,7 @@ defmodule Code.Normalizer do
# Bit containers # Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state, state.escape) normalize_bitstring(quoted, state)
end end
# Atoms with interpolations # Atoms with interpolations
@@ -91,7 +89,13 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state) normalize_literal(:utf8, [], state)
end end
string = normalize_bitstring(string, state, state.escape) string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]} {{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end end
@@ -114,7 +118,6 @@ defmodule Code.Normalizer do
end end
end) end)
parts = maybe_add_trailing_newline(call_meta, parts, state)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} {{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else else
normalize_call(quoted, state) normalize_call(quoted, state)
@@ -184,8 +187,7 @@ defmodule Code.Normalizer do
|> patch_meta_line(state.parent_meta) |> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"") |> Keyword.put_new(:delimiter, "\"")
string = normalize_bitstring(string, %{state | parent_meta: meta}, false) {sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [string, modifiers]}
else else
_ -> _ ->
normalize_call(quoted, state) normalize_call(quoted, state)
@@ -267,7 +269,7 @@ defmodule Code.Normalizer do
"Elixir." <> segments -> "Elixir." <> segments ->
segments segments
|> String.split(".") |> String.split(".")
|> Enum.map(&String.to_unsafe_atom/1) |> Enum.map(&String.to_atom/1)
end end
{:__aliases__, meta, segments} {:__aliases__, meta, segments}
@@ -349,20 +351,18 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta}) args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args} {form, meta, args}
Keyword.has_key?(meta, :do) and kw_blocks?(last) -> Keyword.has_key?(meta, :do) ->
# def foo do :ok end # def foo do :ok end
# def foo, do: :ok # def foo, do: :ok
normalize_kw_blocks(form, meta, args, state) normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and kw_blocks?(last) -> match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
# Non normalized kw blocks # Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line] line = state.parent_meta[:line] || meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]] meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state) normalize_kw_blocks(form, meta, args, state)
true -> true ->
# The formatter renders do-end blocks from the meta alone
meta = Keyword.drop(meta, [:do, :end])
args = normalize_args(args, %{state | parent_meta: meta}) args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, []) {last_arg, leading_args} = List.pop_at(args, -1, [])
@@ -401,22 +401,11 @@ defmodule Code.Normalizer do
defp block_keyword?([]), do: true defp block_keyword?([]), do: true
defp block_keyword?(_), do: false defp block_keyword?(_), do: false
# Anything after the do block that is not a block keyword makes it a keyword list
defp kw_blocks?([{:do, _} | rest] = kw) do
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?(_), do: false
defp allow_keyword?(:when, 2), do: true defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity) defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
meta = patch_meta_line(meta, state.parent_meta) meta = patch_meta_line(meta, state.parent_meta)
parts = parts =
@@ -435,21 +424,9 @@ defmodule Code.Normalizer do
end) end)
end end
parts = maybe_add_trailing_newline(meta, parts, state)
{:<<>>, meta, parts} {:<<>>, meta, parts}
end end
defp maybe_add_trailing_newline(meta, parts, state) do
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
last = List.last(parts),
true <- is_binary(last) and not String.ends_with?(last, "\n") do
[_last | rest] = Enum.reverse(parts)
Enum.reverse([last <> "\n" | rest])
else
_ -> parts
end
end
defp normalize_interpolation_parts(parts, state, escape_interpolation) do defp normalize_interpolation_parts(parts, state, escape_interpolation) do
Enum.map(parts, fn Enum.map(parts, fn
{:"::", interpolation_meta, {:"::", interpolation_meta,
@@ -575,7 +552,7 @@ defmodule Code.Normalizer do
atom atom
|> Atom.to_string() |> Atom.to_string()
|> maybe_escape_literal(state) |> maybe_escape_literal(state)
|> String.to_unsafe_atom() |> String.to_atom()
end end
defp maybe_escape_literal(term, _) do defp maybe_escape_literal(term, _) do
+9 -11
View File
@@ -80,8 +80,7 @@ defmodule Code.Typespec do
Returns all types available from the module's BEAM code. Returns all types available from the module's BEAM code.
The result is returned as a list of tuples where the first The result is returned as a list of tuples where the first
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+, element is the type (`:typep`, `:type` and `:opaque`).
`:nominal`).
The module must have a corresponding BEAM file which can be The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang located by the runtime system. The types will be in the Erlang
@@ -96,10 +95,9 @@ defmodule Code.Typespec do
types = types =
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code, for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
kind in [:opaque, :type, :nominal] do kind in [:opaque, :type] do
cond do cond do
kind == :opaque -> {:opaque, type} kind == :opaque -> {:opaque, type}
kind == :nominal -> {:nominal, type}
{name, length(args)} in exported_types -> {:type, type} {name, length(args)} in exported_types -> {:type, type}
true -> {:typep, type} true -> {:typep, type}
end end
@@ -119,7 +117,7 @@ defmodule Code.Typespec do
element is spec name and arity and the second is the spec. element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file which can be The module must have a corresponding BEAM file which can be
located by the runtime system. The specs will be in the Erlang located by the runtime system. The types will be in the Erlang
Abstract Format. Abstract Format.
""" """
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error @spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
@@ -137,10 +135,10 @@ defmodule Code.Typespec do
Returns all callbacks available from the module's BEAM code. Returns all callbacks available from the module's BEAM code.
The result is returned as a list of tuples where the first The result is returned as a list of tuples where the first
element is the callback name and arity and the second is the callback. element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file The module must have a corresponding BEAM file
which can be located by the runtime system. The callbacks will be which can be located by the runtime system. The types will be
in the Erlang Abstract Format. in the Erlang Abstract Format.
""" """
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error @spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
@@ -193,8 +191,8 @@ defmodule Code.Typespec do
## To AST conversion ## To AST conversion
defp collect_vars({:ann_type, _anno, [_var, type]}) do defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
collect_vars(type) []
end end
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
@@ -401,10 +399,10 @@ defmodule Code.Typespec do
defp erl_to_ex_var(var) do defp erl_to_ex_var(var) do
case Atom.to_string(var) do case Atom.to_string(var) do
<<"_", c::utf8, rest::binary>> -> <<"_", c::utf8, rest::binary>> ->
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}") String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
<<c::utf8, rest::binary>> -> <<c::utf8, rest::binary>> ->
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}") String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
end end
end end
-16
View File
@@ -69,22 +69,6 @@ defprotocol Collectable do
iex> Enum.into([1, 2, 3], MapSet.new()) iex> Enum.into([1, 2, 3], MapSet.new())
MapSet.new([1, 2, 3]) MapSet.new([1, 2, 3])
## Halting
The `:halt` flag will be given whenever the collection won't
terminate correctly and must be used to clean up existing resources
(such as sockets, file handles, etc).
Note it is not guaranteed that the accumulator given to halt will
be the latest version of the accumulator returned by a previous call
with `{:cont, elem}`. Therefore, you must track the collected results
within the resource you intend to halt.
This is by design: ensuring halt is always called with the latest
accumulator would make pure collectables (the ones that do not implement
halt) expensive. However, given the collectables that must implement halt
already need to track state, the burden of tracking the accumulator
across invocations is put on them.
""" """
@type command :: {:cont, term} | :done | :halt @type command :: {:cont, term} | :done | :halt
+10 -19
View File
@@ -152,7 +152,6 @@ defmodule Config do
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec config(atom(), keyword()) :: keyword()
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do if not Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}" raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
@@ -199,7 +198,6 @@ defmodule Config do
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec config(atom(), atom(), term()) :: keyword()
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
get_config!() get_config!()
|> __merge__([{root_key, [{key, opts}]}]) |> __merge__([{root_key, [{key, opts}]}])
@@ -227,7 +225,6 @@ defmodule Config do
""" """
@doc since: "1.18.0" @doc since: "1.18.0"
@spec read_config(atom()) :: keyword() | nil
def read_config(root_key) when is_atom(root_key) do def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key] get_config!()[root_key]
end end
@@ -236,7 +233,7 @@ defmodule Config do
Returns the environment this configuration file is executed on. Returns the environment this configuration file is executed on.
In Mix projects this function returns the environment this configuration In Mix projects this function returns the environment this configuration
file is executed on. file is executed on.
In releases, returns the `MIX_ENV` specified when running `mix release`. In releases, returns the `MIX_ENV` specified when running `mix release`.
This is most often used to execute conditional code: This is most often used to execute conditional code:
@@ -287,8 +284,8 @@ defmodule Config do
In case the file doesn't exist, an error is raised. In case the file doesn't exist, an error is raised.
If the file is relative, it will be expanded relative to the If file is a relative, it will be expanded relatively to the
directory of the current configuration file. directory the current configuration file is in.
## Examples ## Examples
@@ -380,27 +377,21 @@ defmodule Config do
end end
end end
defp validate!(config, file) when is_list(config) do defp validate!(config, file) do
Enum.each(config, fn Enum.all?(config, fn
{app, value} when is_atom(app) -> {app, value} when is_atom(app) ->
if not Keyword.keyword?(value) do if Keyword.keyword?(value) do
true
else
raise ArgumentError, raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <> "expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}" "to return keyword list, got: #{inspect(value)}"
end end
other -> _ ->
raise ArgumentError, false
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
end) end)
config config
end end
defp validate!(config, file) do
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got: #{inspect(config)}"
end
end end
+1 -1
View File
@@ -25,7 +25,7 @@ defmodule Config.Provider do
For example, imagine you want to list some basic configuration For example, imagine you want to list some basic configuration
on Mix's built-in `config/runtime.exs` file, but you also want on Mix's built-in `config/runtime.exs` file, but you also want
to support additional configuration files. To do so, you can add to support additional configuration files. To do so, you can add
this inside the `def project` portion of your `mix.exs`: this inside the `def project` portion of your `mix.exs`:
releases: [ releases: [
demo: [ demo: [
+1 -1
View File
@@ -16,7 +16,7 @@ defmodule Config.Reader do
For example, if you expect the target system to have a config file For example, if you expect the target system to have a config file
in an absolute path, you can add this inside the `def project` portion in an absolute path, you can add this inside the `def project` portion
of your `mix.exs`: of your `mix.exs`:
releases: [ releases: [
demo: [ demo: [
+4 -5
View File
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
## Examples ## Examples
A dynamic supervisor is started with no children and often with a name: A dynamic supervisor is started with no children and often a name:
children = [ children = [
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one} {DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
@@ -206,7 +206,6 @@ defmodule DynamicSupervisor do
See `Supervisor` for more information about child specifications. See `Supervisor` for more information about child specifications.
""" """
@doc since: "1.6.1" @doc since: "1.6.1"
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
def child_spec(options) when is_list(options) do def child_spec(options) when is_list(options) do
id = id =
case Keyword.get(options, :name, DynamicSupervisor) do case Keyword.get(options, :name, DynamicSupervisor) do
@@ -481,11 +480,11 @@ defmodule DynamicSupervisor do
end end
@doc """ @doc """
Returns a list with information about all children of the given supervisor. Returns a list with information about all children.
Note that calling this function when supervising a large number Note that calling this function when supervising a large number
of children under low memory conditions can bring the system down due to an of children under low memory conditions can cause an out of memory
out of memory error. exception.
This function returns a list of tuples containing: This function returns a list of tuples containing:
+81 -234
View File
@@ -39,20 +39,6 @@ defprotocol Enumerable do
`reduce/3` function. All other functions exist as optimizations paths `reduce/3` function. All other functions exist as optimizations paths
for data structures that can implement certain properties in better for data structures that can implement certain properties in better
than linear time. than linear time.
## Default implementation for lists
Sometimes you may want to implement this protocol for a list contained
in struct. This can be done by delegating to the `Enumerable.List` module
in the `reduce/3` implementation and providing a straight-forward
implementation for the remaining ones:
defimpl Enumerable, for: CustomStruct do
def count(struct), do: {:ok, length(struct.items)}
def member?(struct, value), do: {:ok, value in struct.items}
def slice(struct), do: {:error, __MODULE__}
def reduce(struct, acc, fun), do: Enumerable.List.reduce(struct.items, acc, fun)
end
""" """
@typedoc """ @typedoc """
@@ -618,7 +604,7 @@ defmodule Enum do
acc, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), (element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc}) (acc -> {:cont, chunk, acc} | {:cont, acc})
) :: [chunk] ) :: Enumerable.t()
when chunk: any when chunk: any
def chunk_while(enumerable, acc, chunk_fun, after_fun) do def chunk_while(enumerable, acc, chunk_fun, after_fun) do
{_, {res, acc}} = {_, {res, acc}} =
@@ -666,7 +652,7 @@ defmodule Enum do
[1, [2], 3, 4, 5, 6] [1, [2], 3, 4, 5, 6]
""" """
@spec concat(Enumerable.t(Enumerable.t(elem))) :: [elem] when elem: term @spec concat(t) :: t
def concat(enumerables) def concat(enumerables)
def concat(list) when is_list(list) do def concat(list) when is_list(list) do
@@ -681,8 +667,8 @@ defmodule Enum do
Concatenates the enumerable on the `right` with the enumerable on the Concatenates the enumerable on the `right` with the enumerable on the
`left`. `left`.
This function behaves similarly to the `++/2` operator with proper This function produces the same result as the `++/2` operator
lists, but applied to enumerables. for lists.
## Examples ## Examples
@@ -693,7 +679,7 @@ defmodule Enum do
[1, 2, 3, 4, 5, 6] [1, 2, 3, 4, 5, 6]
""" """
@spec concat(Enumerable.t(elem), Enumerable.t(elem)) :: [elem] when elem: term @spec concat(t, t) :: t
def concat(left, right) when is_list(left) and is_list(right) do def concat(left, right) when is_list(left) and is_list(right) do
left ++ right left ++ right
end end
@@ -780,10 +766,6 @@ defmodule Enum do
end end
end end
def count_until(_enumerable, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """ @doc """
Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`. Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`.
@@ -805,10 +787,6 @@ defmodule Enum do
end end
end end
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """ @doc """
Enumerates the `enumerable`, returning a list where all consecutive Enumerates the `enumerable`, returning a list where all consecutive
duplicate elements are collapsed to a single element. duplicate elements are collapsed to a single element.
@@ -829,7 +807,7 @@ defmodule Enum do
""" """
@spec dedup(t) :: list @spec dedup(t) :: list
def dedup(enumerable) when is_list(enumerable) do def dedup(enumerable) when is_list(enumerable) do
dedup_list(enumerable) dedup_list(enumerable, []) |> :lists.reverse()
end end
def dedup(enumerable) do def dedup(enumerable) do
@@ -859,10 +837,6 @@ defmodule Enum do
""" """
@spec dedup_by(t, (element -> term)) :: list @spec dedup_by(t, (element -> term)) :: list
def dedup_by([head | tail], fun) do
dedup_by_list(tail, fun, fun.(head), [head])
end
def dedup_by(enumerable, fun) do def dedup_by(enumerable, fun) do
{list, _} = reduce(enumerable, {[], []}, R.dedup(fun)) {list, _} = reduce(enumerable, {[], []}, R.dedup(fun))
:lists.reverse(list) :lists.reverse(list)
@@ -907,7 +881,7 @@ defmodule Enum do
def drop(enumerable, amount) when is_integer(amount) and amount < 0 do def drop(enumerable, amount) when is_integer(amount) and amount < 0 do
{count, fun} = slice_count_and_fun(enumerable, 1) {count, fun} = slice_count_and_fun(enumerable, 1)
amount = amount + count amount = Kernel.min(amount + count, count)
if amount > 0 do if amount > 0 do
fun.(0, amount, 1) fun.(0, amount, 1)
@@ -977,8 +951,8 @@ defmodule Enum do
## Examples ## Examples
Enum.each(["some", "example"], fn x -> IO.puts(x) end) Enum.each(["some", "example"], fn x -> IO.puts(x) end)
some "some"
example "example"
#=> :ok #=> :ok
""" """
@@ -1240,7 +1214,7 @@ defmodule Enum do
end end
@doc """ @doc """
Maps the given `fun` over `enumerable` and flattens the result only one level deep. Maps the given `fun` over `enumerable` and flattens the result.
This function returns a new enumerable built by appending the result of invoking `fun` This function returns a new enumerable built by appending the result of invoking `fun`
on each element of `enumerable` together; conceptually, this is similar to a on each element of `enumerable` together; conceptually, this is similar to a
@@ -1257,7 +1231,7 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end) iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[:a], [:b], [:c]] [[:a], [:b], [:c]]
This is frequently used to transform and filter in one pass, returning empty This is frequently used to to transform and filter in one pass, returning empty
lists to exclude results: lists to exclude results:
iex> Enum.flat_map([4, 0, 2, 0], fn x -> iex> Enum.flat_map([4, 0, 2, 0], fn x ->
@@ -1288,16 +1262,13 @@ defmodule Enum do
defp flat_reverse([], acc), do: acc defp flat_reverse([], acc), do: acc
@doc """ @doc """
Maps and reduces an `enumerable`, flattening the results only one level deep. Maps and reduces an `enumerable`, flattening the given results (only one level deep).
It expects an accumulator and a function that receives each enumerable It expects an accumulator and a function that receives each enumerable
element, and must return a tuple containing a new enumerable (often a list) element, and must return a tuple containing a new enumerable (often a list)
with the new accumulator or a tuple with `:halt` as first element and with the new accumulator or a tuple with `:halt` as first element and
the accumulator as second. the accumulator as second.
Returns a 2-element tuple where the first element is the results flattened one level deep and
the second element is the last accumulator.
## Examples ## Examples
iex> enumerable = 1..100 iex> enumerable = 1..100
@@ -1446,7 +1417,7 @@ defmodule Enum do
) )
# Avoid warnings about Dict # Avoid warnings about Dict
dict_module = String.to_unsafe_atom("Dict") dict_module = String.to_atom("Dict")
reduce(reverse(enumerable), dict, fn entry, categories -> reduce(reverse(enumerable), dict, fn entry, categories ->
dict_module.update(categories, fun.(entry), [entry], &[entry | &1]) dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
@@ -1522,14 +1493,6 @@ defmodule Enum do
to_list(enumerable) to_list(enumerable)
end end
def into(enumerable, collectable) when is_struct(collectable, MapSet) do
if MapSet.size(collectable) == 0 do
MapSet.new(enumerable)
else
MapSet.new(enumerable) |> MapSet.union(collectable)
end
end
def into(%_{} = enumerable, collectable) do def into(%_{} = enumerable, collectable) do
into_protocol(enumerable, collectable) into_protocol(enumerable, collectable)
end end
@@ -1606,12 +1569,8 @@ defmodule Enum do
map(enumerable, transform) map(enumerable, transform)
end end
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do def into(%_{} = enumerable, collectable, transform) do
if MapSet.size(collectable) == 0 do into_protocol(enumerable, collectable, transform)
MapSet.new(enumerable, transform)
else
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
end
end end
def into(enumerable, %_{} = collectable, transform) do def into(enumerable, %_{} = collectable, transform) do
@@ -1883,7 +1842,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` according Returns the maximal element in the `enumerable` according
to Erlang's term ordering. to Erlang's term ordering.
By default, the comparison is done with the [`>=`](`>=/2`) sorter function. By default, the comparison is done with the `>=` sorter function.
If multiple elements are considered maximal, the first one that If multiple elements are considered maximal, the first one that
was found is returned. If you want the last element considered was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true maximal to be returned, the sorter function should not return true
@@ -1950,7 +1909,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` as calculated Returns the maximal element in the `enumerable` as calculated
by the given `fun`. by the given `fun`.
By default, the comparison is done with the [`>=`](`>=/2`) sorter function. By default, the comparison is done with the `>=` sorter function.
If multiple elements are considered maximal, the first one that If multiple elements are considered maximal, the first one that
was found is returned. If you want the last element considered was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true maximal to be returned, the sorter function should not return true
@@ -2027,16 +1986,11 @@ defmodule Enum do
operators work by using this function. operators work by using this function.
""" """
@spec member?(t, element) :: boolean @spec member?(t, element) :: boolean
def member?(enumerable, element) do def member?(enumerable, element) when is_list(enumerable) do
__in__(element, enumerable)
end
@doc false
def __in__(element, enumerable) when is_list(enumerable) do
:lists.member(element, enumerable) :lists.member(element, enumerable)
end end
def __in__(element, enumerable) do def member?(enumerable, element) do
case Enumerable.member?(enumerable, element) do case Enumerable.member?(enumerable, element) do
{:ok, element} when is_boolean(element) -> {:ok, element} when is_boolean(element) ->
element element
@@ -2068,7 +2022,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` according Returns the minimal element in the `enumerable` according
to Erlang's term ordering. to Erlang's term ordering.
By default, the comparison is done with the [`<=`](`<=/2`) sorter function. By default, the comparison is done with the `<=` sorter function.
If multiple elements are considered minimal, the first one that If multiple elements are considered minimal, the first one that
was found is returned. If you want the last element considered was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true minimal to be returned, the sorter function should not return true
@@ -2135,7 +2089,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` as calculated Returns the minimal element in the `enumerable` as calculated
by the given `fun`. by the given `fun`.
By default, the comparison is done with the [`<=`](`<=/2`) sorter function. By default, the comparison is done with the `<=` sorter function.
If multiple elements are considered minimal, the first one that If multiple elements are considered minimal, the first one that
was found is returned. If you want the last element considered was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true minimal to be returned, the sorter function should not return true
@@ -2189,65 +2143,28 @@ defmodule Enum do
@doc """ @doc """
Returns a tuple with the minimal and the maximal elements in the Returns a tuple with the minimal and the maximal elements in the
enumerable. enumerable according to Erlang's term ordering.
By default, the comparison is done with the [`<`](`</2`) sorter function, If multiple elements are considered maximal or minimal, the first one
as the function must not return true for equal elements. that was found is returned.
Calls the provided `empty_fallback` function and returns its value if
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
## Examples ## Examples
iex> Enum.min_max([2, 3, 1]) iex> Enum.min_max([2, 3, 1])
{1, 3} {1, 3}
iex> Enum.min_max(["foo", "bar", "baz"])
{"bar", "foo"}
iex> Enum.min_max([], fn -> {nil, nil} end) iex> Enum.min_max([], fn -> {nil, nil} end)
{nil, nil} {nil, nil}
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
iex> dates = [
...> ~D[2019-01-01],
...> ~D[2020-01-01],
...> ~D[2018-01-01]
...> ]
iex> Enum.min_max(dates, Date)
{~D[2018-01-01], ~D[2020-01-01]}
You can also pass a custom sorting function:
iex> Enum.min_max([2, 3, 1], &>/2)
{3, 1}
Finally, if you don't want to raise on empty enumerables, you can pass
the empty fallback:
iex> Enum.min_max([], fn -> nil end)
nil
""" """
@spec min_max(t, (element, element -> boolean) | module()) :: {min :: element, max :: element} @spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
@spec min_max(t, (-> empty_result)) :: {min :: element, max :: element} | empty_result
when empty_result: any
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
{min :: element, max :: element} | empty_result
when empty_result: any when empty_result: any
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end) def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
def min_max(list = [_ | _], empty_fallback) when is_function(empty_fallback, 0) do
min_max_list(list)
end
def min_max(first..last//step = range, empty_fallback)
when is_function(empty_fallback, 0) do
case Range.size(range) do case Range.size(range) do
0 -> 0 ->
empty_fallback.() empty_fallback.()
@@ -2258,39 +2175,11 @@ defmodule Enum do
end end
end end
def min_max(enumerable, empty_fallback) def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
when is_function(empty_fallback, 0) do
min_max(enumerable, &</2, empty_fallback)
end
def min_max(enumerable, sorter) when is_atom(sorter) do
min_max(enumerable, min_max_sort_fun(sorter))
end
def min_max(enumerable, sorter) when is_function(sorter, 2) do
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
end
def min_max(enumerable, sorter, empty_fallback)
when is_atom(sorter) and is_function(empty_fallback, 0) do
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
end
def min_max(enumerable, sorter, empty_fallback)
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
first_fun = &[&1 | &1] first_fun = &[&1 | &1]
reduce_fun = fn entry, [min | max] = acc -> reduce_fun = fn entry, [min | max] ->
cond do [Kernel.min(min, entry) | Kernel.max(max, entry)]
sorter.(entry, min) ->
[entry | max]
sorter.(max, entry) ->
[min | entry]
true ->
acc
end
end end
case reduce_by(enumerable, first_fun, reduce_fun) do case reduce_by(enumerable, first_fun, reduce_fun) do
@@ -2311,8 +2200,8 @@ defmodule Enum do
Returns a tuple with the minimal and the maximal elements in the Returns a tuple with the minimal and the maximal elements in the
enumerable as calculated by the given function. enumerable as calculated by the given function.
By default, the comparison is done with the [`<`](`</2`) sorter function, If multiple elements are considered maximal or minimal, the first one
as the function must not return `true` for equal elements. that was found is returned.
## Examples ## Examples
@@ -2352,14 +2241,14 @@ defmodule Enum do
""" """
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) :: @spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
{min :: element, max :: element} | empty_result {element, element} | empty_result
when empty_result: any when empty_result: any
@spec min_max_by( @spec min_max_by(
t, t,
(element -> any), (element -> any),
(element, element -> boolean) | module(), (element, element -> boolean) | module(),
(-> empty_result) (-> empty_result)
) :: {min :: element, max :: element} | empty_result ) :: {element, element} | empty_result
when empty_result: any when empty_result: any
def min_max_by( def min_max_by(
enumerable, enumerable,
@@ -2370,7 +2259,7 @@ defmodule Enum do
def min_max_by(enumerable, fun, sorter, empty_fallback) def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback) min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
end end
def min_max_by(enumerable, fun, sorter, empty_fallback) def min_max_by(enumerable, fun, sorter, empty_fallback)
@@ -2401,19 +2290,7 @@ defmodule Enum do
end end
end end
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt) defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
defp min_max_list([h | t]), do: min_max_list(t, h, h)
defp min_max_list([h | t], min, max) do
cond do
h < min -> min_max_list(t, h, max)
max < h -> min_max_list(t, min, h)
true -> min_max_list(t, min, max)
end
end
defp min_max_list([], min, max), do: {min, max}
@doc """ @doc """
Splits the `enumerable` in two lists according to the given function `fun`. Splits the `enumerable` in two lists according to the given function `fun`.
@@ -2692,7 +2569,7 @@ defmodule Enum do
5050 5050
""" """
@spec reduce_while(t, acc, (element, acc -> {:cont, acc} | {:halt, acc})) :: acc @spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
def reduce_while(enumerable, acc, fun) do def reduce_while(enumerable, acc, fun) do
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1) Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
end end
@@ -2935,7 +2812,8 @@ defmodule Enum do
end end
defp slide_list_middle(list, 0, last, start_to_middle) do defp slide_list_middle(list, 0, last, start_to_middle) do
slide_list_last(list, last + 1, [], start_to_middle) {slid_range, tail} = slide_list_last(list, last + 1, [])
slid_range ++ :lists.reverse(start_to_middle, tail)
end end
# You asked for a middle index off the end of the list... you get what we've got # You asked for a middle index off the end of the list... you get what we've got
@@ -2943,16 +2821,16 @@ defmodule Enum do
:lists.reverse(acc) :lists.reverse(acc)
end end
defp slide_list_last([h | t], last, acc, start_to_middle) when last > 0 do defp slide_list_last([h | t], last, acc) when last > 0 do
slide_list_last(t, last - 1, [h | acc], start_to_middle) slide_list_last(t, last - 1, [h | acc])
end end
defp slide_list_last(rest, 0, acc, start_to_middle) do defp slide_list_last(rest, 0, acc) do
:lists.reverse(acc, :lists.reverse(start_to_middle, rest)) {:lists.reverse(acc), rest}
end end
defp slide_list_last([], _, acc, start_to_middle) do defp slide_list_last([], _, acc) do
:lists.reverse(acc, :lists.reverse(start_to_middle)) {:lists.reverse(acc), []}
end end
@doc """ @doc """
@@ -4216,11 +4094,6 @@ defmodule Enum do
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end) iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
[4, 6] [4, 6]
`zip_with/2` can be used to transpose lists of lists:
iex> Enum.zip_with([[1, 2], [3, 4]], & &1)
[[1, 3], [2, 4]]
""" """
@doc since: "1.12.0" @doc since: "1.12.0"
@spec zip_with(t, ([term] -> term)) :: [term] @spec zip_with(t, ([term] -> term)) :: [term]
@@ -4329,24 +4202,11 @@ defmodule Enum do
empty.() empty.()
_ -> _ ->
# The endpoint shortcut is only valid for sorters consistent with last = last - rem(last - first, step)
# the natural integer order of the range elements, which is known
# to hold for the default sorters; any other sorter traverses the
# elements, seeded with the first one since the range is not empty
if fun == (&<=/2) or fun == (&>=/2) do
last = last - rem(last - first, step)
case fun.(first, last) do case fun.(first, last) do
true -> first true -> first
false -> last false -> last
end
else
reduce_range(first + step, last, step, first, fn element, acc ->
case fun.(acc, element) do
true -> acc
false -> element
end
end)
end end
end end
end end
@@ -4449,9 +4309,10 @@ defmodule Enum do
## any?/2 all?/2 ## any?/2 all?/2
defp predicate_list([h | t], initial, fun) do defp predicate_list([h | t], initial, fun) do
case !!fun.(h) do if !!fun.(h) == initial do
^initial -> predicate_list(t, initial, fun) predicate_list(t, initial, fun)
_ -> not initial else
not initial
end end
end end
@@ -4462,9 +4323,10 @@ defmodule Enum do
defp predicate_range(first, last, step, initial, fun) defp predicate_range(first, last, step, initial, fun)
when step > 0 and first <= last when step > 0 and first <= last
when step < 0 and first >= last do when step < 0 and first >= last do
case !!fun.(first) do if !!fun.(first) == initial do
^initial -> predicate_range(first + step, last, step, initial, fun) predicate_range(first + step, last, step, initial, fun)
_ -> not initial else
not initial
end end
end end
@@ -4543,20 +4405,19 @@ defmodule Enum do
# dedup # dedup
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail) defp dedup_list([value | tail], acc) do
defp dedup_list([value | tail]), do: [value | dedup_list(tail)] acc =
defp dedup_list([]), do: [] case acc do
[^value | _] -> acc
_ -> [value | acc]
end
## dedup_by dedup_list(tail, acc)
defp dedup_by_list([head | tail], fun, prev, acc) do
case fun.(head) do
^prev -> dedup_by_list(tail, fun, prev, acc)
new_val -> dedup_by_list(tail, fun, new_val, [head | acc])
end
end end
defp dedup_by_list([], _fun, _prev, acc), do: :lists.reverse(acc) defp dedup_list([], acc) do
acc
end
## drop ## drop
@@ -5150,7 +5011,8 @@ end
defimpl Enumerable, for: List do defimpl Enumerable, for: List do
def count(list), do: {:ok, length(list)} def count(list), do: {:ok, length(list)}
def member?(list, value), do: {:ok, :lists.member(value, list)} def member?([], _value), do: {:ok, false}
def member?(_list, _value), do: {:error, __MODULE__}
def slice([]), do: {:ok, 0, fn _, _, _ -> [] end} def slice([]), do: {:ok, 0, fn _, _, _ -> [] end}
def slice(_list), do: {:error, __MODULE__} def slice(_list), do: {:error, __MODULE__}
@@ -5205,16 +5067,7 @@ defimpl Enumerable, for: Range do
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
reduce = def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
quote generated: true do
reduce(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(acc),
var!(fun)
)
end
def unquote(reduce) do
step = if first <= last, do: 1, else: -1 step = if first <= last, do: 1, else: -1
reduce(Map.put(range, :step, step), acc, fun) reduce(Map.put(range, :step, step), acc, fun)
end end
@@ -5237,12 +5090,12 @@ defimpl Enumerable, for: Range do
{:done, acc} {:done, acc}
end end
def member?(first..last//step, value) when is_integer(value) and step > 0 do def member?(first..last//step, value) when is_integer(value) do
{:ok, first <= value and value <= last and rem(value - first, step) == 0} if step > 0 do
end {:ok, first <= value and value <= last and rem(value - first, step) == 0}
else
def member?(first..last//step, value) when is_integer(value) and step < 0 do {:ok, last <= value and value <= first and rem(value - first, step) == 0}
{:ok, last <= value and value <= first and rem(value - first, step) == 0} end
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
@@ -5261,17 +5114,11 @@ defimpl Enumerable, for: Range do
end end
def slice(first.._//step = range) do def slice(first.._//step = range) do
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)} {:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
def slice(%{__struct__: Range, first: first, last: last} = range) do
slice =
quote generated: true do
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
end
def unquote(slice) do
step = if first <= last, do: 1, else: -1 step = if first <= last, do: 1, else: -1
slice(Map.put(range, :step, step)) slice(Map.put(range, :step, step))
end end
+40 -23
View File
@@ -26,7 +26,7 @@ defmodule Exception do
@typedoc "The exception type" @typedoc "The exception type"
@type t :: %{ @type t :: %{
required(:__struct__) => module, required(:__struct__) => module,
required(:__exception__) => term, required(:__exception__) => true,
optional(atom) => any optional(atom) => any
} }
@@ -77,7 +77,7 @@ defmodule Exception do
@doc false @doc false
@deprecated "Use Kernel.is_exception/1 instead" @deprecated "Use Kernel.is_exception/1 instead"
def exception?(term) def exception?(term)
def exception?(%_{__exception__: _}), do: true def exception?(%_{__exception__: true}), do: true
def exception?(_), do: false def exception?(_), do: false
@doc """ @doc """
@@ -89,7 +89,7 @@ defmodule Exception do
return a descriptive error message instead. return a descriptive error message instead.
""" """
@spec message(t) :: String.t() @spec message(t) :: String.t()
def message(%module{__exception__: _} = exception) do def message(%module{__exception__: true} = exception) do
try do try do
module.message(exception) module.message(exception)
rescue rescue
@@ -123,7 +123,7 @@ defmodule Exception do
@spec normalize(:error, any, stacktrace) :: t @spec normalize(:error, any, stacktrace) :: t
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var @spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
def normalize(kind, payload, stacktrace \\ []) def normalize(kind, payload, stacktrace \\ [])
def normalize(:error, %_{__exception__: _} = payload, _stacktrace), do: payload def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace) def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
def normalize(_kind, payload, _stacktrace), do: payload def normalize(_kind, payload, _stacktrace), do: payload
@@ -287,10 +287,10 @@ defmodule Exception do
end end
end end
defp map_node?({:is_map, _, [_]}), do: true defp is_map_node?({:is_map, _, [_]}), do: true
defp map_node?(_), do: false defp is_map_node?(_), do: false
defp map_key_node?({:is_map_key, _, [_, _]}), do: true defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
defp map_key_node?(_), do: false defp is_map_key_node?(_), do: false
defp struct_validation_node?( defp struct_validation_node?(
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]} {:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
@@ -304,16 +304,16 @@ defmodule Exception do
defp struct_validation_node?(_), do: false defp struct_validation_node?(_), do: false
defp struct_macro?( defp is_struct_macro?(
{:and, _, {:and, _,
[ [
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]}, {:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]},
%{node: node_3 = {_, _, [{_, _, [_, arg]}]}} %{node: node_3 = {_, _, [{_, _, [_, arg]}]}}
]} ]}
), ),
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3) do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
defp struct_macro?( defp is_struct_macro?(
{:and, _, {:and, _,
[ [
{:and, _, {:and, _,
@@ -328,12 +328,12 @@ defmodule Exception do
%{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}} %{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}}
]} ]}
), ),
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3) do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
defp struct_macro?(_), do: false defp is_struct_macro?(_), do: false
defp translate_guard(guard) do defp translate_guard(guard) do
if struct_macro?(guard) do if is_struct_macro?(guard) do
undo_is_struct_guard(guard) undo_is_struct_guard(guard)
else else
guard guard
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
iex> raise "oops!" iex> raise "oops!"
** (RuntimeError) oops! ** (RuntimeError) oops!
You should use this exception sparingly, since most of the time it might be You should use this exceptions sparingly, since most of the time it might be
better to define your own exceptions specific to your application or library. better to define your own exceptions specific to your application or library.
Sometimes, however, there are situations in which you don't expect a condition to Sometimes, however, there are situations in which you don't expect a condition to
happen, but you want to give a meaningful error message if it does. In those cases, happen, but you want to give a meaningful error message if it does. In those cases,
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
For example, this can happen if you try to create an atom that is too large: For example, this can happen if you try to create an atom that is too large:
iex> String.to_unsafe_atom(String.duplicate("a", 100_000)) iex> String.to_atom(String.duplicate("a", 100_000))
** (SystemLimitError) a system limit has been reached ** (SystemLimitError) a system limit has been reached
""" """
@@ -1388,7 +1388,6 @@ defmodule CompileError do
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if * `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
the error occurred in code that did not come from a file the error occurred in code that did not come from a file
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred * `:line` (`t:non_neg_integer/0`) - the line where the error occurred
* `:description` (`t:String.t/0`) - a description of the compile error
This is mostly raised by Elixir tooling when compiling and evaluating code. This is mostly raised by Elixir tooling when compiling and evaluating code.
""" """
@@ -1457,6 +1456,20 @@ defmodule BadFunctionError do
end end
end end
defmodule BadStructError do
@moduledoc deprecated:
"This exception is deprecated alongside the struct update syntax that raises it"
defexception [:struct, :term]
@impl true
def message(exception) do
Exception.__format_message_with_term__(
"expected a struct named #{inspect(exception.struct)}, got:",
exception.term
)
end
end
defmodule BadMapError do defmodule BadMapError do
@moduledoc """ @moduledoc """
An exception raised when a map is expected, but something else was given. An exception raised when a map is expected, but something else was given.
@@ -1807,7 +1820,7 @@ defmodule UndefinedFunctionError do
defp load_module({name, _path, _loaded?}) do defp load_module({name, _path, _loaded?}) do
name name
|> List.to_unsafe_atom() |> List.to_atom()
|> Code.ensure_loaded() |> Code.ensure_loaded()
end end
@@ -1930,8 +1943,8 @@ defmodule FunctionClauseError do
For example: For example:
iex> List.duplicate(:ok, -3) iex> URI.parse(:wrong_argument)
** (FunctionClauseError) no function clause matching in List.duplicate/2 ** (FunctionClauseError) no function clause matching in URI.parse/1
The following fields of this exception are public and can be accessed freely: The following fields of this exception are public and can be accessed freely:
@@ -2376,7 +2389,7 @@ defmodule File.Error do
@moduledoc """ @moduledoc """
An exception that is raised when a file operation fails. An exception that is raised when a file operation fails.
For example, this exception is raised, when trying to read a nonexistent file: For example, this exception is raised, when trying to read a non existent file:
iex> File.read!("nonexistent_file.txt") iex> File.read!("nonexistent_file.txt")
** (File.Error) could not read file "nonexistent_file.txt": no such file or directory ** (File.Error) could not read file "nonexistent_file.txt": no such file or directory
@@ -2409,7 +2422,7 @@ defmodule File.CopyError do
@moduledoc """ @moduledoc """
An exception that is raised when copying a file fails. An exception that is raised when copying a file fails.
For example, this exception is raised when trying to copy to a file or directory that isn't present: For example, this exception is raised when trying to copy to file or directory that isn't present:
iex> File.cp_r!("non_existent", "source_dir/subdir") iex> File.cp_r!("non_existent", "source_dir/subdir")
** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory ** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory
@@ -2477,7 +2490,7 @@ defmodule File.LinkError do
@moduledoc """ @moduledoc """
An exception that is raised when linking a file fails. An exception that is raised when linking a file fails.
For example, this exception is raised when trying to link to a file that isn't present: For example, this exception is raised when trying to link to file that isn't present:
iex> File.ln!("existing.txt", "link.txt") iex> File.ln!("existing.txt", "link.txt")
** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory ** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory
@@ -2591,6 +2604,10 @@ defmodule ErlangError do
%BadFunctionError{term: term} %BadFunctionError{term: term}
end end
def normalize({:badstruct, struct, term}, _stacktrace) do
%BadStructError{struct: struct, term: term}
end
def normalize({:badmatch, term}, _stacktrace) do def normalize({:badmatch, term}, _stacktrace) do
%MatchError{term: term} %MatchError{term: term}
end end
+30 -121
View File
@@ -400,13 +400,6 @@ defmodule File do
You can use `:file.format_error/1` to get a descriptive string of the error. You can use `:file.format_error/1` to get a descriptive string of the error.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples ## Examples
File.read("hello.txt") File.read("hello.txt")
@@ -415,24 +408,15 @@ defmodule File do
File.read("non_existing.txt") File.read("non_existing.txt")
#=> {:error, :enoent} #=> {:error, :enoent}
""" """
@spec read(Path.t(), [exists_option]) :: @spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit} def read(path) do
when exists_option: :raw :file.read_file(IO.chardata_to_string(path))
def read(path, opts \\ []) do
:file.read_file(IO.chardata_to_string(path), opts)
end end
@doc """ @doc """
Returns a binary with the contents of the given filename, Returns a binary with the contents of the given filename,
or raises a `File.Error` exception if an error occurs. or raises a `File.Error` exception if an error occurs.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples ## Examples
File.read!("hello.txt") File.read!("hello.txt")
@@ -441,9 +425,9 @@ defmodule File do
File.read!("non_existing.txt") File.read!("non_existing.txt")
** (File.Error) could not read file "non_existing.txt": no such file or directory ** (File.Error) could not read file "non_existing.txt": no such file or directory
""" """
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw @spec read!(Path.t()) :: binary
def read!(path, opts \\ []) do def read!(path) do
case read(path, opts) do case read(path) do
{:ok, binary} -> {:ok, binary} ->
binary binary
@@ -710,7 +694,7 @@ defmodule File do
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}}) File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
{:error, :enoent} {:error, :enoent}
File.touch("/tmp/a.txt", 1_544_519_753) File.touch("/tmp/a.txt", 1544519753)
#=> :ok #=> :ok
""" """
@@ -749,7 +733,7 @@ defmodule File do
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}}) File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory ** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
File.touch!("/tmp/a.txt", 1_544_519_753) File.touch!("/tmp/a.txt", 1544519753)
""" """
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok @spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
@@ -897,7 +881,7 @@ defmodule File do
File.copy("non_existing.txt", "copy.txt") File.copy("non_existing.txt", "copy.txt")
#=> {:error, :enoent} #=> {:error, :enoent}
""" """
@spec copy(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) :: @spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated} {:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
def copy(source, destination, bytes_count \\ :infinity) do def copy(source, destination, bytes_count \\ :infinity) do
source = normalize_path_or_io_device(source) source = normalize_path_or_io_device(source)
@@ -918,7 +902,7 @@ defmodule File do
File.copy!("non_existing.txt", "copy.txt") File.copy!("non_existing.txt", "copy.txt")
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory ** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
""" """
@spec copy!(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) :: @spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
non_neg_integer non_neg_integer
def copy!(source, destination, bytes_count \\ :infinity) do def copy!(source, destination, bytes_count \\ :infinity) do
case copy(source, destination, bytes_count) do case copy(source, destination, bytes_count) do
@@ -1103,7 +1087,7 @@ defmodule File do
@doc ~S""" @doc ~S"""
Copies the contents in `source` to `destination` recursively, maintaining the Copies the contents in `source` to `destination` recursively, maintaining the
source directory structure and regular file modes. source directory structure and modes.
If `source` is a file or a symbolic link to it, `destination` must be a path If `source` is a file or a symbolic link to it, `destination` must be a path
to an existent file, a symbolic link to one, or a path to a non-existent file. to an existent file, a symbolic link to one, or a path to a non-existent file.
@@ -1114,9 +1098,7 @@ defmodule File do
If the source is a file, it copies `source` to `destination`. If the `source` If the source is a file, it copies `source` to `destination`. If the `source`
is a directory, it copies the contents inside source into the `destination` directory. is a directory, it copies the contents inside source into the `destination` directory.
For regular files, their respective file modes are preserved in the destination. If a file already exists in the destination, it invokes the optional `on_conflict`
Directory modes are preserved only when `:preserve_directory_permissions` is `true`.
If a file already exists in the destination, it invokes the optional `:on_conflict`
callback given as an option. See "Options" for more information. callback given as an option. See "Options" for more information.
This function may fail while copying files, in such cases, it will leave the This function may fail while copying files, in such cases, it will leave the
@@ -1132,14 +1114,6 @@ defmodule File do
explicitly disallow this behavior. If `source` is a `file` and `destination` explicitly disallow this behavior. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned. is a directory, `{:error, :eisdir}` will be returned.
Special files such as device files, sockets, and named pipes are not copied.
Typical error reasons are:
* `:enoent` - `source` does not exist
* `:eisdir` - `source` is a file and `destination` is a directory
* `:einval` - `destination` is the same as or a subdirectory of `source`
## Options ## Options
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination. * `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
@@ -1153,11 +1127,6 @@ defmodule File do
dereferenced and have their contents copied instead when set to `true`. If the dereferenced dereferenced and have their contents copied instead when set to `true`. If the dereferenced
files do not exist, than the operation fails. The default is `false`. files do not exist, than the operation fails. The default is `false`.
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
source directories are copied to the destination directories after their contents are
written. This is useful when source directories are read-only or have restricted
permissions that must be preserved. The default is `false`.
## Examples ## Examples
# Copies file "a.txt" to "b.txt" # Copies file "a.txt" to "b.txt"
@@ -1175,16 +1144,11 @@ defmodule File do
#=> {:ok, ["z.txt", "y.txt", "x.txt]} #=> {:ok, ["z.txt", "y.txt", "x.txt]}
File.cp_r("non_existing.txt", "copy.txt") File.cp_r("non_existing.txt", "copy.txt")
#=> {:error, :enoent, "non_existing.txt"} #=> {:error, :enoent}
# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
""" """
@spec cp_r(Path.t(), Path.t(), @spec cp_r(Path.t(), Path.t(),
on_conflict: on_conflict_callback, on_conflict: on_conflict_callback,
dereference_symlinks: boolean(), dereference_symlinks: boolean()
preserve_directory_permissions: boolean()
) :: ) ::
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary} {:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
@@ -1206,7 +1170,6 @@ defmodule File do
def cp_r(source, destination, options) when is_list(options) do def cp_r(source, destination, options) when is_list(options) do
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end) on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
dereference? = Keyword.get(options, :dereference_symlinks, false) dereference? = Keyword.get(options, :dereference_symlinks, false)
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
source = source =
source source
@@ -1218,25 +1181,9 @@ defmodule File do
|> IO.chardata_to_string() |> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3") |> assert_no_null_byte!("File.cp_r/3")
source_parts = source |> Path.expand() |> Path.split() case do_cp_r(source, destination, on_conflict, dereference?, []) do
dest_parts = destination |> Path.expand() |> Path.split() {:error, _, _} = error -> error
res -> {:ok, res}
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
{:error, :einval, destination}
else
dereference = if dereference?, do: MapSet.new(), else: nil
case do_cp_r(
source,
destination,
on_conflict,
dereference,
preserve_directory_permissions?,
[]
) do
{:error, _, _} = error -> error
res -> {:ok, res}
end
end end
end end
@@ -1257,8 +1204,7 @@ defmodule File do
""" """
@spec cp_r!(Path.t(), Path.t(), @spec cp_r!(Path.t(), Path.t(),
on_conflict: on_conflict_callback, on_conflict: on_conflict_callback,
dereference_symlinks: boolean(), dereference_symlinks: boolean()
preserve_directory_permissions: boolean()
) :: [binary] ) :: [binary]
def cp_r!(source, destination, options \\ []) do def cp_r!(source, destination, options \\ []) do
case cp_r(source, destination, options) do case cp_r(source, destination, options) do
@@ -1275,7 +1221,7 @@ defmodule File do
end end
end end
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do case :elixir_utils.read_link_type(src) do
{:ok, :regular} -> {:ok, :regular} ->
case do_cp_file(src, dest, on_conflict, acc) do case do_cp_file(src, dest, on_conflict, acc) do
@@ -1288,15 +1234,8 @@ defmodule File do
{:ok, :symlink} -> {:ok, :symlink} ->
case :file.read_link(src) do case :file.read_link(src) do
{:ok, link} when dereference != nil -> {:ok, link} when dereference? ->
resolved = Path.expand(link, Path.dirname(src)) do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
if MapSet.member?(dereference, resolved) do
{:error, :eloop, src}
else
dereference = MapSet.put(dereference, resolved)
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
end
{:ok, link} -> {:ok, link} ->
do_cp_link(link, src, dest, on_conflict, acc) do_cp_link(link, src, dest, on_conflict, acc)
@@ -1310,35 +1249,9 @@ defmodule File do
{:ok, files} -> {:ok, files} ->
case mkdir(dest) do case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] -> success when success in [:ok, {:error, :eexist}] ->
files Enum.reduce(files, [dest | acc], fn x, acc ->
|> Enum.reduce_while([dest | acc], fn x, acc -> do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
case do_cp_r(
Path.join(src, x),
Path.join(dest, x),
on_conflict,
dereference,
preserve_dir_perms?,
acc
) do
{:error, _, _} = error -> {:halt, error}
acc -> {:cont, acc}
end
end) end)
|> case do
{:error, _, _} = error ->
error
files when preserve_dir_perms? ->
# Change the directory after writing files in case
# it was originally read only
case copy_file_mode(src, dest) do
:ok -> files
{:error, reason} -> {:error, reason, src}
end
files ->
files
end
{:error, reason} -> {:error, reason} ->
{:error, reason, dest} {:error, reason, dest}
@@ -1349,7 +1262,7 @@ defmodule File do
end end
{:ok, _} -> {:ok, _} ->
acc {:error, :eio, src}
{:error, reason} -> {:error, reason} ->
{:error, reason, src} {:error, reason, src}
@@ -1357,13 +1270,14 @@ defmodule File do
end end
# If we reach this clause, there was an error while processing a file. # If we reach this clause, there was an error while processing a file.
defp do_cp_r(_, _, _, _, _, acc) do defp do_cp_r(_, _, _, _, acc) do
acc acc
end end
defp copy_file_mode(src, dest) do defp copy_file_mode(src, dest) do
with {:ok, src_fileinfo} <- stat(src) do with {:ok, dest_fileinfo} <- stat(dest),
chmod(dest, src_fileinfo.mode) {:ok, src_fileinfo} <- stat(src) do
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
end end
end end
@@ -2220,7 +2134,7 @@ defmodule File do
type. If you pass, for example, `[encoding: :utf8]` or type. If you pass, for example, `[encoding: :utf8]` or
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream `[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
will use `IO.write/2` and the `String.Chars` protocol to convert the data. will use `IO.write/2` and the `String.Chars` protocol to convert the data.
See `IO.binwrite/2` and `IO.write/2`. See `IO.binwrite/2` and `IO.write/2` .
One may also consider passing the `:delayed_write` option if the stream One may also consider passing the `:delayed_write` option if the stream
is meant to be written to under a tight loop. is meant to be written to under a tight loop.
@@ -2251,12 +2165,7 @@ defmodule File do
def stream!(path, line_or_bytes, modes) def stream!(path, line_or_bytes, modes)
def stream!(path, modes, line_or_bytes) when is_list(modes) do def stream!(path, modes, line_or_bytes) when is_list(modes) do
# TODO: Remove me on Elixir 2.0 # TODO: Deprecate this on Elixir v1.20
IO.warn(
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
"invoke File.stream!(path, line_or_bytes, modes) instead"
)
stream!(path, line_or_bytes, modes) stream!(path, line_or_bytes, modes)
end end
+7 -20
View File
@@ -18,13 +18,7 @@ defmodule File.Stream do
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
@type t :: %__MODULE__{ @type t :: %__MODULE__{}
path: Path.t(),
modes: [term()],
line_or_bytes: :line | pos_integer(),
raw: boolean(),
node: node()
}
@doc false @doc false
def __build__(path, line_or_bytes, modes) do def __build__(path, line_or_bytes, modes) do
@@ -125,7 +119,7 @@ defmodule File.Stream do
counter = fn device -> counter = fn device ->
device = skip_bom_and_offset(device, raw, modes) device = skip_bom_and_offset(device, raw, modes)
count_lines(device, path, pattern, read_function(stream), 0, :empty) count_lines(device, path, pattern, read_function(stream), 0)
end end
{:ok, open!(stream, modes, counter)} {:ok, open!(stream, modes, counter)}
@@ -235,28 +229,21 @@ defmodule File.Stream do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
end end
defp count_lines(device, path, pattern, read, count, last_byte) do defp count_lines(device, path, pattern, read, count) do
case read.(device) do case read.(device) do
data when is_binary(data) and byte_size(data) > 0 ->
newlines = length(:binary.matches(data, pattern))
last = :binary.last(data)
count_lines(device, path, pattern, read, count + newlines, last)
data when is_binary(data) -> data when is_binary(data) ->
count_lines(device, path, pattern, read, count, last_byte) count_lines(device, path, pattern, read, count + count_lines(data, pattern))
:eof -> :eof ->
case last_byte do count
:empty -> 0
?\n -> count
_ -> count + 1
end
{:error, reason} -> {:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path raise File.Error, reason: reason, action: "stream", path: path
end end
end end
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size) defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size) defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
end end
+142 -191
View File
@@ -25,7 +25,7 @@ defmodule Float do
and arithmetic due to the fact most decimal fractions cannot be and arithmetic due to the fact most decimal fractions cannot be
represented by a floating-point binary and most operations are not exact, represented by a floating-point binary and most operations are not exact,
but operate on approximations. Those issues are not specific but operate on approximations. Those issues are not specific
to Elixir, they are a property of floating-point representation itself. to Elixir, they are a property of floating point representation itself.
For example, the numbers 0.1 and 0.01 are two of them, what means the result For example, the numbers 0.1 and 0.01 are two of them, what means the result
of squaring 0.1 does not give 0.01 neither the closest representable. Here is of squaring 0.1 does not give 0.01 neither the closest representable. Here is
@@ -42,7 +42,7 @@ defmodule Float do
To learn more about floating-point arithmetic visit: To learn more about floating-point arithmetic visit:
* [0.30000000000000004.com](https://0.30000000000000004.com/) * [0.30000000000000004.com](http://0.30000000000000004.com/)
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/) * [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
""" """
@@ -167,73 +167,51 @@ defmodule Float do
parse_unsigned(binary) parse_unsigned(binary)
end end
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9, defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_mantissa(binary, rest, false) do: parse_unsigned(rest, false, false, [digit])
defp parse_unsigned(binary) when is_binary(binary), do: :error defp parse_unsigned(binary) when is_binary(binary), do: :error
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9, defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_mantissa(binary, rest, dot?) do: parse_unsigned(rest, dot?, e?, [digit | acc])
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9, defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_mantissa(binary, rest, true) do: parse_unsigned(rest, true, false, [digit, ?. | acc])
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?) defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and digit in ?0..?9, when exp_marker in ~c"eE" and digit in ?0..?9,
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?) defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9, when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?) # When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9, # rescue the ArgumentError here and return an error.
do: parse_exponent(binary, exp_pos, rest, dot?) defp parse_unsigned(rest, dot?, true = _e?, acc) do
acc
defp parse_exponent(binary, exp_pos, rest, dot?), |> add_dot(dot?)
do: finish_exponent(binary, exp_pos, rest, dot?) |> :lists.reverse()
|> :erlang.list_to_float()
defp finish_mantissa(binary, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue rescue
ArgumentError -> :error ArgumentError -> :error
else
float -> {float, rest}
end end
# Bare integer: * 1.0 casts to the nearest float without building a new binary, defp parse_unsigned(rest, dot?, false = _e?, acc) do
# and raises ArithmeticError on overflow (for example a 400-digit integer). float =
defp finish_mantissa(binary, rest, _dot? = false) do acc
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest} |> add_dot(dot?)
rescue |> :lists.reverse()
ArithmeticError -> :error |> :erlang.list_to_float()
{float, rest}
end end
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400". defp add_dot(acc, true), do: acc
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do defp add_dot(acc, false), do: [?0, ?. | acc]
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
end
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
# form a valid float literal.
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
len = byte_size(binary) - byte_size(rest)
literal =
IO.iodata_to_binary([
:binary.part(binary, 0, exp_pos),
".0",
:binary.part(binary, exp_pos, len - exp_pos)
])
{:erlang.binary_to_float(literal), rest}
rescue
ArgumentError -> :error
end
defp consumed(binary, ""), do: binary
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
@doc """ @doc """
Rounds a float to the largest float less than or equal to `number`. Rounds a float to the largest float less than or equal to `number`.
@@ -286,7 +264,7 @@ defmodule Float do
@doc """ @doc """
Rounds a float to the smallest float greater than or equal to `number`. Rounds a float to the smallest float greater than or equal to `number`.
`ceil/2` also accepts a precision to round a floating-point value up `ceil/2` also accepts a precision to round a floating-point value down
to an arbitrary number of fractional digits (between 0 and 15). to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a The operation is performed on the binary floating point, without a
@@ -355,7 +333,7 @@ defmodule Float do
and therefore the number above is internally represented as 5.567499999, and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals, 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 you must use a decimal library. The behavior above is also in accordance
with reference implementations, such as "Correctly Rounded Binary-Decimal and to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay. Decimal-Binary Conversions" by David M. Gay.
## Examples ## Examples
@@ -377,12 +355,15 @@ defmodule Float do
""" """
@spec round(float, precision_range) :: float @spec round(float, precision_range) :: float
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
def round(float, precision \\ 0) def round(float, precision \\ 0)
def round(float, 0) when float === 0.0 or float === -0.0, do: float def round(float, 0) when float == 0.0, do: float
def round(float, 0) when is_float(float) do def round(float, 0) when is_float(float) do
case :erlang.round(float) * 1.0 do case float |> :erlang.round() |> :erlang.float() do
zero when zero == 0.0 and float < 0.0 -> -0.0 zero when zero == 0.0 and float < 0.0 -> -0.0
rounded -> rounded rounded -> rounded
end end
@@ -396,170 +377,140 @@ defmodule Float do
raise ArgumentError, invalid_precision_message(precision) raise ArgumentError, invalid_precision_message(precision)
end end
# Decimal-place rounding via exact rational scaling. This is the bignum
# core used by reference implementations like David M. Gay's "Correctly
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
# @doc above), Python's round(), and Java's BigDecimal.setScale.
#
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
# Because precision is bounded to 0..15, the product fits in ~103 bits
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
# directly without approximation.
# 3. Round the exact rational to an integer per the requested mode
# (half_up / floor / ceil) using quotient and remainder.
# 4. Emit the float closest to rounded_int / 10^precision:
# - fast path: when rounded_int < 2^53, both operands are exactly
# representable as floats and IEEE division is correctly rounded.
# - slow path: bignum alignment + manual mantissa extraction with
# round-to-nearest-even for the trailing bit.
#
# The integer-rounding decision (step 3) and the binary-emission decision
# (step 4) are deliberately independent: step 3 picks the exact rational
# the user asked for, step 4 picks the closest float to that rational.
# Conflating them is the classic source of double-rounding bugs.
#
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
# for round-trip printing) but target different problems or assume
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
# At precision <= 15, the exact path is small, easy to audit, and fast
# enough that a more complex algorithm has not been justified by benchmarks.
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
defp round(float, precision, mode) do defp round(float, precision, rounding) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>> <<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
cond do cond do
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump # Precision beyond 15 digits
# to 10^-precision; everything else rounds to signed zero). count >= 104 ->
exp == 0 -> case rounding do
tiny_round(sign, precision, mode) :ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
:ceil when sign === 1 -> minus_zero()
:half_up when sign === 1 -> minus_zero()
_ -> 0.0
end
# |float| >= 2^52 — has no fractional bits, return unchanged. # We are asking more precision than we have
exp - 1075 >= 0 -> count <= precision ->
float float
true -> true ->
mantissa = @power_of_2_to_52 ||| mantissa # Difference in precision between float and asked precision
shift = 1075 - exp # We subtract 1 because we need to calculate the remainder too
do_round(sign, mantissa, shift, precision, mode) diff = count - precision - 1
# Get up to latest so we calculate the remainder
power_of_10 = power_of_10(diff)
# Convert the numerand to decimal base
num = num * power_of_5(count)
# Move to the given precision - 1
num = div(num, power_of_10)
div = div(num, 10)
num = rounding(rounding, sign, num, div)
# Convert back to float without loss
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
den = power_of_10(precision)
boundary = den <<< 52
cond do
num == 0 and sign == 1 ->
minus_zero()
num == 0 ->
0.0
num >= boundary ->
{den, exp} = scale_down(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
true ->
{num, exp} = scale_up(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
end
end end
end end
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign. # TODO remove once we require Erlang/OTP 27+
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do # This function tricks the compiler to avoid this bug in previous versions:
tiny_round(sign, precision, mode) # https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
defp minus_zero, do: -0.0
defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
end end
defp do_round(sign, mantissa, shift, precision, mode) do defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
power = power_of_10(precision) decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
product = mantissa * power end
half = 1 <<< (shift - 1)
quotient = product >>> shift
remainder = product - (quotient <<< shift)
rounded_int = round_step(mode, sign, quotient, remainder, half)
cond do defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
rounded_int == 0 -> decompose(bits, count + 1, last_count, acc)
signed_zero(sign) end
rounded_int < @power_of_2_to_52 <<< 1 -> defp decompose(<<>>, _count, last_count, acc) do
# Both rounded_int and power fit in 53 bits, so IEEE float division {acc, last_count}
# is correctly rounded. end
result = rounded_int / power
if sign == 1, do: -result, else: result
true -> defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
bignum_to_float(sign, rounded_int, power) defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
defp scale_down(num, den, exp) do
new_den = den <<< 1
if num < new_den do
{den >>> 52, exp}
else
scale_down(num, new_den, exp + 1)
end end
end end
defp round_step(:half_up, _sign, quotient, remainder, half) do defp decimal_to_float(sign, num, den, exp) do
if remainder >= half, do: quotient + 1, else: quotient quo = div(num, den)
end rem = num - quo * den
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient tmp =
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1 case den >>> 1 do
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient den when rem > den -> quo + 1
den when rem < den -> quo
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1 _ when (quo &&& 1) === 1 -> quo + 1
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient _ -> quo
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
defp signed_zero(0), do: 0.0
defp signed_zero(1), do: -0.0
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
# Slow path: emit float closest to `sign * rounded_int / power` when
# rounded_int >= 2^53. The binary emission step is always IEEE
# round-to-nearest-even, regardless of the integer-rounding mode.
defp bignum_to_float(sign, rounded_int, power) do
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
quotient = div(numerator, denominator)
remainder = numerator - quotient * denominator
half = denominator >>> 1
mantissa =
cond do
remainder > half -> quotient + 1
remainder < half -> quotient
(quotient &&& 1) === 1 -> quotient + 1
true -> quotient
end end
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper tmp = tmp - @power_of_2_to_52
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient <<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
# or when rounding carries. Rebalance into the canonical [2^52, 2^53) tmp
# range so the 52-bit packing below doesn't silently truncate.
{mantissa, exp} =
if mantissa == @power_of_2_to_52 <<< 1,
do: {@power_of_2_to_52, exp + 1},
else: {mantissa, exp}
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
result
end end
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53) defp rounding(:floor, 1, _num, div), do: div + 1
# and the resulting float = numerator/denominator * 2^(exp-52). defp rounding(:ceil, 0, _num, div), do: div + 1
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
new_power = power <<< shift_adjust
if rounded_int < new_power <<< 53, defp rounding(:half_up, _sign, num, div) do
do: {rounded_int, new_power, 52 + shift_adjust}, case rem(num, 10) do
else: {rounded_int, new_power <<< 1, 53 + shift_adjust} rem when rem < 5 -> div
end rem when rem >= 5 -> div + 1
defp align(rounded_int, power, shift_adjust) do
shifted = rounded_int <<< -shift_adjust
cond do
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
true -> {shifted <<< 1, power, 51 + shift_adjust}
end end
end end
defp bit_length(0), do: 0 defp rounding(_, _, _, div), do: div
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
defp bit_length(_integer, acc), do: acc
Enum.reduce(0..15, 1, fn exponent, acc -> Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_10(unquote(exponent)), do: unquote(acc) defp power_of_10(unquote(x)), do: unquote(acc)
acc * 10 acc * 10
end) end)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end)
@doc """ @doc """
Returns a pair of integers whose ratio is exactly equal Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator. to the original float and with a positive denominator.
@@ -705,7 +656,7 @@ defmodule Float do
end end
defp invalid_precision_message(precision) do defp invalid_precision_message(precision) do
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}" "precision #{precision} is out of valid range of #{inspect(@precision_range)}"
end end
defp expand_compact([{:compact, false} | t]), do: expand_compact(t) defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
+6 -1
View File
@@ -69,6 +69,7 @@ defmodule Function do
| :name | :name
| :new_index | :new_index
| :new_uniq | :new_uniq
| :pid
| :type | :type
| :uniq | :uniq
@@ -111,6 +112,8 @@ defmodule Function do
When `fun` is an anonymous function (that is, the type is `:local`), the following When `fun` is an anonymous function (that is, the type is `:local`), the following
additional keys are returned: additional keys are returned:
* `:pid` - PID of the process that originally created the function.
* `:index` - (integer) an index into the module function table. * `:index` - (integer) an index into the module function table.
* `:new_index` - (integer) an index into the module function table. * `:new_index` - (integer) an index into the module function table.
@@ -156,7 +159,7 @@ defmodule Function do
`:module`, `:name`, `:arity`, `:env`, or `:type`. `:module`, `:name`, `:arity`, `:env`, or `:type`.
For anonymous functions, there is also information about any of the For anonymous functions, there is also information about any of the
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`. atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
For a named function, the value of any of these items is always the For a named function, the value of any of these items is always the
atom `:undefined`. atom `:undefined`.
@@ -176,6 +179,8 @@ defmodule Function do
iex> fun = &String.length/1 iex> fun = &String.length/1
iex> Function.info(fun, :name) iex> Function.info(fun, :name)
{:name, :length} {:name, :length}
iex> Function.info(fun, :pid)
{:pid, :undefined}
""" """
@doc since: "1.7.0" @doc since: "1.7.0"
+1 -1
View File
@@ -36,7 +36,7 @@ defmodule GenEvent do
alternative. GenStage is an external Elixir library maintained by the Elixir alternative. GenStage is an external Elixir library maintained by the Elixir
team; it provides a tool to implement systems that exchange events in a team; it provides a tool to implement systems that exchange events in a
demand-driven way with built-in support for back-pressure. See the [GenStage demand-driven way with built-in support for back-pressure. See the [GenStage
documentation](https://gen-stage.hexdocs.pm) for more information. documentation](https://hexdocs.pm/gen_stage) for more information.
### `:gen_event` ### `:gen_event`
+8 -49
View File
@@ -210,14 +210,13 @@ defmodule GenServer do
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the * [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
child should be restarted, defaults to `:permanent` child should be restarted, defaults to `:permanent`
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to * [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
shut down the child, either immediately or by giving it time to shut down, shut down the child, either immediately or by giving it time to shut down
defaults to `5_000`
For example: For example:
use GenServer, restart: :transient, shutdown: 10_000 use GenServer, restart: :transient, shutdown: 10_000
See the ["Child specification"](`m:Supervisor#module-child-specification`) section in the `Supervisor` module for more See the ["Child specification"](`m:Supervisor#module-child_spec-1-function`) section in the `Supervisor` module for more
detailed information. The `@doc` annotation immediately preceding detailed information. The `@doc` annotation immediately preceding
`use GenServer` will be attached to the generated `child_spec/1` function. `use GenServer` will be attached to the generated `child_spec/1` function.
@@ -232,8 +231,6 @@ defmodule GenServer do
a name on start via the `:name` option. Registered names are also a name on start via the `:name` option. Registered names are also
automatically cleaned up on termination. The supported values are: automatically cleaned up on termination. The supported values are:
* `nil` (default) - the GenServer is not registered with a name.
* an atom - the GenServer is registered locally (to the current node) * an atom - the GenServer is registered locally (to the current node)
with the given name using `Process.register/2`. with the given name using `Process.register/2`.
@@ -352,41 +349,6 @@ defmodule GenServer do
message arriving, `handle_info/2` is called with `:timeout` as the first message arriving, `handle_info/2` is called with `:timeout` as the first
argument. argument.
For example:
defmodule Counter do
use GenServer
@timeout to_timeout(second: 5)
@impl true
def init(count) do
{:ok, count, @timeout}
end
@impl true
def handle_call(:increment, _from, count) do
new_count = count + 1
{:reply, new_count, new_count, @timeout}
end
@impl true
def handle_info(:timeout, count) do
{:stop, :normal, count}
end
end
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
after the initialization or after the last `:increment` call:
{:ok, counter_pid} = GenServer.start(Counter, 50)
GenServer.call(counter_pid, :increment)
#=> 51
# After 5 seconds
Process.alive?(counter_pid)
#=> false
## When (not) to use a GenServer ## When (not) to use a GenServer
So far, we have learned that a `GenServer` can be used as a supervised process So far, we have learned that a `GenServer` can be used as a supervised process
@@ -526,7 +488,7 @@ defmodule GenServer do
* [GenServer - Elixir's Getting Started Guide](genservers.md) * [GenServer - Elixir's Getting Started Guide](genservers.md)
* [`:gen_server` module documentation](`:gen_server`) * [`:gen_server` module documentation](`:gen_server`)
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html) * [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers) * [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
""" """
@@ -567,12 +529,9 @@ defmodule GenServer do
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2` `Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
after a delay to attempt a restart. after a delay to attempt a restart.
Returning `{:error, reason}` will cause `start_link/3` to return Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}`. `{:error, reason}` and the process to exit with reason `reason` without
entering the loop or calling `c:terminate/2`.
Returning `{:stop, reason}` will the process to exit with reason `reason`,
without entering the loop or calling `c:terminate/2`. `start_link/3` will
return `{:error, reason}`, but only if the caller is trapping exits.
""" """
@callback init(init_arg :: term) :: @callback init(init_arg :: term) ::
{:ok, state} {:ok, state}
@@ -863,7 +822,7 @@ defmodule GenServer do
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term} @type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@typedoc "The GenServer name" @typedoc "The GenServer name"
@type name :: nil | atom | {:global, term} | {:via, module, term} @type name :: atom | {:global, term} | {:via, module, term}
@typedoc "Options used by the `start*` functions" @typedoc "Options used by the `start*` functions"
@type options :: [option] @type options :: [option]
@@ -1158,7 +1117,7 @@ defmodule GenServer do
## Timeouts ## Timeouts
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds to wait for a reply, or the atom `:infinity` to wait milliseconds to wait for a reply, or the atom `:infinity` to wait
indefinitely. The default value is `5000`. If no reply is received within indefinitely. The default value is `5000`. If no reply is received within
the specified time, the function call fails and the caller exits. If the the specified time, the function call fails and the caller exits. If the
+5 -5
View File
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
module.reduce(dict, acc, fun) module.reduce(dict, acc, fun)
end end
def member?(dict, {key, value}) do def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
{:ok, match?({:ok, ^value}, module.fetch(dict, key))} {:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end end
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do def count(dict) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
{:ok, module.size(dict)} {:ok, module.size(dict)}
end end
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
def into(original) do def into(original) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
collector_fun = fn collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value) dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
def inspect(dict, opts) do def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated. # Avoid warnings about HashDict being deprecated.
module = String.to_unsafe_atom("HashDict") module = String.to_atom("HashDict")
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"]) concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end end
end end
+5 -5
View File
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
module.reduce(set, acc, fun) module.reduce(set, acc, fun)
end end
def member?(set, term) do def member?(set, term) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
{:ok, module.member?(set, term)} {:ok, module.member?(set, term)}
end end
def count(set) do def count(set) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
{:ok, module.size(set)} {:ok, module.size(set)}
end end
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
def into(original) do def into(original) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
collector_fun = fn collector_fun = fn
set, {:cont, term} -> module.put(set, term) set, {:cont, term} -> module.put(set, term)
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
def inspect(set, opts) do def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated. # Avoid warnings about HashSet being deprecated.
module = String.to_unsafe_atom("HashSet") module = String.to_atom("HashSet")
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"]) concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end end
end end
+6 -17
View File
@@ -92,7 +92,7 @@ defprotocol Inspect do
end end
inspect(%Point{x: 1}) inspect(%Point{x: 1})
#=> %Point{x: 1, y: 0} %Point{x: 1, y: 0}
## Custom implementation ## Custom implementation
@@ -275,6 +275,8 @@ defprotocol Inspect do
end end
defimpl Inspect, for: Atom do defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts) color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end end
@@ -436,8 +438,8 @@ defimpl Inspect, for: List do
@doc false @doc false
def keyword?([{key, _value} | rest]) when is_atom(key) do def keyword?([{key, _value} | rest]) when is_atom(key) do
case Atom.to_string(key) do case Atom.to_charlist(key) do
"Elixir." <> _ -> false [?E, ?l, ?i, ?x, ?i, ?r, ?.] ++ _ -> false
_ -> keyword?(rest) _ -> keyword?(rest)
end end
end end
@@ -689,11 +691,6 @@ defimpl Inspect, for: Any do
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts) Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
end end
# A temporary clause to deal with native records until they are officially supported
def inspect(native_record, _opts) do
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
end
def inspect_as_struct(map, name, infos, opts) do def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts) open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts) sep = color_doc(",", :map, opts)
@@ -721,15 +718,7 @@ defimpl Inspect, for: Range do
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
inspect = def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
quote generated: true do
inspect(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(opts)
)
end
def unquote(inspect) do
step = if first <= last, do: 1, else: -1 step = if first <= last, do: 1, else: -1
inspect(Map.put(range, :step, step), opts) inspect(Map.put(range, :step, step), opts)
end end
+3 -3
View File
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
* `:limit` - limits the number of items that are inspected for tuples, * `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items, with the exception of bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option. printable strings and printable charlists which use the `:printable_limit` option.
It accepts a positive integer or `:infinity`. It defaults to `200` since It accepts a positive integer or `:infinity`. It defaults to `100` since
`Elixir v1.20.0`, as it has better defaults to deal with nested collections. `Elixir v1.19.0`, as it has better defaults to deal with nested collections.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`. * `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
charlists: :infer, charlists: :infer,
custom_options: [], custom_options: [],
inspect_fun: &Inspect.inspect/2, inspect_fun: &Inspect.inspect/2,
limit: 200, limit: 100,
pretty: false, pretty: false,
printable_limit: 4096, printable_limit: 4096,
safe: true, safe: true,
+6 -84
View File
@@ -18,45 +18,6 @@ defmodule Integer do
import Bitwise import Bitwise
@doc """
Counts the number of set bits (1) in the binary representation of a non-negative `integer`.
This operation is known as the Hamming weight or population count.
Raises an `ArithmeticError` if `integer` is negative.
## Examples
iex> Integer.popcount(0)
0
iex> Integer.popcount(1)
1
iex> Integer.popcount(0b10110101)
5
iex> Integer.popcount(255)
8
iex> Integer.popcount(0b1111111111111111)
16
iex> Integer.popcount(-1)
** (ArithmeticError) bad argument in arithmetic expression
"""
@doc since: "1.20.0"
@spec popcount(non_neg_integer) :: non_neg_integer
def popcount(integer) when is_integer(integer) and integer < 0,
do: :erlang.error(:badarith, [integer])
def popcount(integer) when is_integer(integer),
do: popcount(integer, 0)
defp popcount(0, acc), do: acc
defp popcount(n, acc), do: popcount(n &&& n - 1, acc + 1)
@doc """ @doc """
Determines if `integer` is odd. Determines if `integer` is odd.
@@ -108,7 +69,7 @@ defmodule Integer do
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0 defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
@doc """ @doc """
Computes `base` raised to the power of `exponent`. Computes `base` raised to power of `exponent`.
Both `base` and `exponent` must be integers. Both `base` and `exponent` must be integers.
The exponent must be zero or positive. The exponent must be zero or positive.
@@ -211,35 +172,6 @@ defmodule Integer do
end end
end end
@doc """
Performs a ceiled integer division.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
This function performs a *ceiled* integer division, which means that
the result will always be rounded towards positive infinity.
## Examples
iex> Integer.ceil_div(5, 2)
3
iex> Integer.ceil_div(6, -4)
-1
iex> Integer.ceil_div(-99, 2)
-49
"""
@doc since: "1.20.0"
@spec ceil_div(integer, neg_integer | pos_integer) :: integer
def ceil_div(dividend, divisor) do
if not :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
div(dividend, divisor) + 1
else
div(dividend, divisor)
end
end
@doc """ @doc """
Returns the ordered digits for the given `integer`. Returns the ordered digits for the given `integer`.
@@ -297,9 +229,8 @@ defmodule Integer do
defp undigits([], _base, acc), do: acc defp undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
when is_integer(digit) and (digit >= base or digit <= -base), do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(digit), defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit) do: undigits(tail, base, acc * base + digit)
@@ -310,7 +241,7 @@ defmodule Integer do
An optional `base` to the corresponding integer can be provided. An optional `base` to the corresponding integer can be provided.
If `base` is not given, 10 will be used. If `base` is not given, 10 will be used.
If successful, returns a tuple in the form of `{integer, remaining_string}`. If successful, returns a tuple in the form of `{integer, remainder_of_binary}`.
Otherwise `:error`. Otherwise `:error`.
Raises an error if `base` is less than 2 or more than 36. Raises an error if `base` is less than 2 or more than 36.
@@ -329,9 +260,6 @@ defmodule Integer do
iex> Integer.parse("three") iex> Integer.parse("three")
:error :error
iex> Integer.parse("404 not found")
{404, " not found"}
iex> Integer.parse("34", 10) iex> Integer.parse("34", 10)
{34, ""} {34, ""}
@@ -532,12 +460,8 @@ defmodule Integer do
iex> Integer.extended_gcd(10, 0) iex> Integer.extended_gcd(10, 0)
{10, 1, 0} {10, 1, 0}
iex> Integer.extended_gcd(-10, 0)
{10, -1, 0}
iex> Integer.extended_gcd(0, 10) iex> Integer.extended_gcd(0, 10)
{10, 0, 1} {10, 0, 1}
iex> Integer.extended_gcd(0, -10)
{10, 0, -1}
iex> Integer.extended_gcd(0, 0) iex> Integer.extended_gcd(0, 0)
{0, 0, 0} {0, 0, 0}
@@ -545,10 +469,8 @@ defmodule Integer do
@doc since: "1.12.0" @doc since: "1.12.0"
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer} @spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
def extended_gcd(0, 0), do: {0, 0, 0} def extended_gcd(0, 0), do: {0, 0, 0}
def extended_gcd(0, b) when is_integer(b) and b > 0, do: {b, 0, 1} def extended_gcd(0, b), do: {b, 0, 1}
def extended_gcd(0, b) when is_integer(b) and b < 0, do: {-b, 0, -1} def extended_gcd(a, 0), do: {a, 1, 0}
def extended_gcd(a, 0) when is_integer(a) and a > 0, do: {a, 1, 0}
def extended_gcd(a, 0) when is_integer(a) and a < 0, do: {-a, -1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0) extended_gcd(integer2, integer1, 0, 1, 1, 0)
+6 -29
View File
@@ -128,7 +128,7 @@ defmodule IO do
@type nodata :: {:error, term} | :eof @type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | []) @type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, String.Chars.t()}] @type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
@typedoc """ @typedoc """
Stacktrace information as keyword options for `warn/2`. Stacktrace information as keyword options for `warn/2`.
@@ -152,9 +152,9 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument: The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it is the number of Unicode * if `line_or_chars` is an integer, it represents a number of bytes. The device is
code points to be retrieved for devices open in Unicode/utf8 mode. iterated by that number of bytes. This should be the preferred mode for reading
Otherwise, it is the number of raw bytes to be retrieved. non-textual inputs.
* if `line_or_chars` is `:line`, the device is iterated line by line. * if `line_or_chars` is `:line`, the device is iterated line by line.
CRLF newlines ("\r\n") are automatically normalized to "\n". CRLF newlines ("\r\n") are automatically normalized to "\n".
@@ -521,7 +521,7 @@ defmodule IO do
end end
@doc """ @doc """
Gets a number of characters from IO device `:stdio`. Gets a number of bytes from IO device `:stdio`.
If `:stdio` is a Unicode device, `count` implies If `:stdio` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved. the number of Unicode code points to be retrieved.
@@ -549,7 +549,7 @@ defmodule IO do
end end
@doc """ @doc """
Gets a number of characters from the IO `device`. Gets a number of bytes from the IO `device`.
If the IO `device` is a Unicode device, `count` implies If the IO `device` is a Unicode device, `count` implies
the number of Unicode code points to be retrieved. the number of Unicode code points to be retrieved.
@@ -796,29 +796,6 @@ defmodule IO do
:erlang.iolist_size(iodata) :erlang.iolist_size(iodata)
end end
@doc """
Checks if an IO data (the length is zero).
For more information about IO data, see the ["IO data"](#module-io-data)
section in the module documentation.
## Examples
iex> IO.iodata_empty?([])
true
iex> IO.iodata_empty?([""])
true
iex> IO.iodata_empty?([1, 2 | <<3, 4>>])
false
"""
@doc since: "1.20.0"
@spec iodata_empty?(iodata) :: boolean
def iodata_empty?(""), do: true
def iodata_empty?([]), do: true
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
def iodata_empty?(_), do: false
@doc false @doc false
def each_stream(device, line_or_codepoints) do def each_stream(device, line_or_codepoints) do
case read(device, line_or_codepoints) do case read(device, line_or_codepoints) do
+7 -9
View File
@@ -111,15 +111,13 @@ defmodule IO.ANSI do
end end
defsequence = fn name, code, terminator -> defsequence = fn name, code, terminator ->
sequence = "\e[#{code}#{terminator}"
@spec unquote(name)() :: String.t() @spec unquote(name)() :: String.t()
def unquote(name)() do def unquote(name)() do
unquote(sequence) "\e[#{unquote(code)}#{unquote(terminator)}"
end end
defp format_sequence(unquote(name)) do defp format_sequence(unquote(name)) do
unquote(sequence) unquote(name)()
end end
end end
@@ -161,7 +159,7 @@ defmodule IO.ANSI do
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
@doc "Sets alternative font #{font_n}." @doc "Sets alternative font #{font_n}."
defsequence.(String.to_unsafe_atom("font_#{font_n}"), font_n + 10, "m") defsequence.(:"font_#{font_n}", font_n + 10, "m")
end end
@doc "Normal color or intensity." @doc "Normal color or intensity."
@@ -195,13 +193,13 @@ defmodule IO.ANSI do
defsequence.(color, code + 30, "m") defsequence.(color, code + 30, "m")
@doc "Sets foreground color to light #{color}." @doc "Sets foreground color to light #{color}."
defsequence.(String.to_unsafe_atom("light_#{color}"), code + 90, "m") defsequence.(:"light_#{color}", code + 90, "m")
@doc "Sets background color to #{color}." @doc "Sets background color to #{color}."
defsequence.(String.to_unsafe_atom("#{color}_background"), code + 40, "m") defsequence.(:"#{color}_background", code + 40, "m")
@doc "Sets background color to light #{color}." @doc "Sets background color to light #{color}."
defsequence.(String.to_unsafe_atom("light_#{color}_background"), code + 100, "m") defsequence.(:"light_#{color}_background", code + 100, "m")
end end
@doc "Default text color." @doc "Default text color."
@@ -334,7 +332,7 @@ defmodule IO.ANSI do
end end
defp do_format([], [], acc, true, true) do defp do_format([], [], acc, true, true) do
[acc | reset()] [acc | IO.ANSI.reset()]
end end
defp do_format([], [], acc, _emit?, _append_reset) do defp do_format([], [], acc, _emit?, _append_reset) do
+1 -1
View File
@@ -673,7 +673,7 @@ defmodule IO.ANSI.Docs do
# Characters that can mark the beginning or the end of a word. # Characters that can mark the beginning or the end of a word.
# Only support the most common ones at this moment. # Only support the most common ones at this moment.
@delimiters [?\s, ?', ?", ?!, ??, ?,, ?:, ?;, ?/, ?@, ?#, ?$, ?%, ?^, ?&] ++ @delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.] [?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
### Inline start ### Inline start
+1 -1
View File
@@ -31,7 +31,7 @@ defmodule IO.Stream do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
device: IO.device(), device: IO.device(),
raw: boolean(), raw: boolean(),
line_or_bytes: :line | pos_integer() line_or_bytes: :line | non_neg_integer()
} }
@doc false @doc false
+17 -23
View File
@@ -8,29 +8,24 @@ defprotocol JSON.Encoder do
If you have a struct, you can derive the implementation of this protocol If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to JSON: by specifying which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [...]} @derive {JSON.Encoder, only: [....]}
defstruct ... defstruct ...
Additionally, you can exclude specific fields using the `:except` option or It is also possible to encode all fields or skip some fields via the
encode all fields by omitting both options entirely, but these should be used `:except` option:
with caution:
@derive {JSON.Encoder, except: [...]}
defstruct ...
@derive JSON.Encoder @derive JSON.Encoder
defstruct ... defstruct ...
> #### Leaking Private Information {: .error} > #### Leaking Private Information {: .error}
> >
> Prefer using `:only` to avoid accidentally leaking private information when > The `:except` approach should be used carefully to avoid
> new fields are added. Other approaches should be used with caution. > accidentally leaking private information when new fields are added.
You can also use `Protocol.derive/3` if you don't own the struct that you want Finally, if you don't own the struct you want to encode to JSON,
to encode to JSON: you may use `Protocol.derive/3` placed outside of any module:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...]) Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct) Protocol.derive(JSON.Encoder, NameOfTheStruct)
""" """
@@ -67,7 +62,7 @@ defprotocol JSON.Encoder do
{io, _prefix} = {io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix -> Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:]) key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,} {[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end) end)
@@ -130,25 +125,25 @@ end
defimpl JSON.Encoder, for: BitString do defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do def encode(value, _encoder) do
:json.encode_binary(value) :elixir_json.encode_binary(value)
end end
end end
defimpl JSON.Encoder, for: List do defimpl JSON.Encoder, for: List do
def encode(value, encoder) do def encode(value, encoder) do
:json.encode_list(value, encoder) :elixir_json.encode_list(value, encoder)
end end
end end
defimpl JSON.Encoder, for: Integer do defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do def encode(value, _encoder) do
:json.encode_integer(value) :elixir_json.encode_integer(value)
end end
end end
defimpl JSON.Encoder, for: Float do defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do def encode(value, _encoder) do
:json.encode_float(value) :elixir_json.encode_float(value)
end end
end end
@@ -175,7 +170,6 @@ defimpl JSON.Encoder, for: Map do
# Erlang supports only numbers, binaries, and atoms as keys, # Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol. # we support anything that implements the String.Chars protocol.
@compile inline: [key: 2]
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder) defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder) defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder) defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
@@ -409,7 +403,7 @@ defmodule JSON do
decoders = Keyword.put_new(decoders, :null, nil) decoders = Keyword.put_new(decoders, :null, nil)
try do try do
:json.decode(binary, acc, Map.new(decoders)) :elixir_json.decode(binary, acc, Map.new(decoders))
catch catch
:error, :unexpected_end -> :error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}} {:error, {:unexpected_end, byte_size(binary)}}
@@ -529,16 +523,16 @@ defmodule JSON do
end end
def protocol_encode(value, _encoder) when is_binary(value), def protocol_encode(value, _encoder) when is_binary(value),
do: :json.encode_binary(value) do: :elixir_json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value), def protocol_encode(value, _encoder) when is_integer(value),
do: :json.encode_integer(value) do: :elixir_json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value), def protocol_encode(value, _encoder) when is_float(value),
do: :json.encode_float(value) do: :elixir_json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value), def protocol_encode(value, encoder) when is_list(value),
do: :json.encode_list(value, encoder) do: :elixir_json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__), def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: JSON.Encoder.Map.encode(value, encoder) do: JSON.Encoder.Map.encode(value, encoder)
+115 -146
View File
@@ -142,7 +142,7 @@ defmodule Kernel do
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns, * [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
guards, and extensions guards, and extensions
* [Syntax reference](syntax-reference.md) - the language syntax reference * [Syntax reference](syntax-reference.md) - the language syntax reference
* [Typespecs reference](typespecs.md) - types and function specifications, including list of types * [Typespecs reference](typespecs.md)- types and function specifications, including list of types
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode * [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
## Guards ## Guards
@@ -1999,12 +1999,6 @@ defmodule Kernel do
{:case, extra ++ meta, args} {:case, extra ++ meta, args}
end end
defp x_is_false_or_nil do
quote generated: true do
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
end
end
@doc """ @doc """
Strictly boolean "or" operator. Strictly boolean "or" operator.
@@ -2069,20 +2063,15 @@ defmodule Kernel do
end end
defp build_boolean_check(operator, check, true_clause, false_clause) do defp build_boolean_check(operator, check, true_clause, false_clause) do
bools =
quote do
false -> unquote(false_clause)
true -> unquote(true_clause)
end
error =
quote generated: true do
other -> :erlang.error({:badbool, unquote(operator), other})
end
annotate_case( annotate_case(
[optimize_boolean: true, type_check: {:case, operator}], [optimize_boolean: true, type_check: :expr],
{:case, [], [check, [do: bools ++ error]]} quote do
case unquote(check) do
false -> unquote(false_clause)
true -> unquote(true_clause)
other -> :erlang.error({:badbool, unquote(operator), other})
end
end
) )
end end
@@ -2109,10 +2098,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!") assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case( annotate_case(
[optimize_boolean: true, type_check: {:case, :"!!"}], [optimize_boolean: true, type_check: :expr],
quote do quote do
case unquote(value) do case unquote(value) do
x when unquote(x_is_false_or_nil()) -> false x when :"Elixir.Kernel".in(x, [false, nil]) -> false
_ -> true _ -> true
end end
end end
@@ -2123,10 +2112,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!") assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case( annotate_case(
[optimize_boolean: true, type_check: {:case, :!}], [optimize_boolean: true, type_check: :expr],
quote do quote do
case unquote(value) do case unquote(value) do
x when unquote(x_is_false_or_nil()) -> true x when :"Elixir.Kernel".in(x, [false, nil]) -> true
_ -> false _ -> false
end end
end end
@@ -2752,7 +2741,7 @@ defmodule Kernel do
nil -> nil ->
quote do quote do
case unquote(term) do case unquote(term) do
%_{__exception__: _} -> true %_{__exception__: true} -> true
_ -> false _ -> false
end end
end end
@@ -2764,7 +2753,8 @@ defmodule Kernel do
quote do quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term)) :erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end end
end end
end end
@@ -2791,7 +2781,7 @@ defmodule Kernel do
case unquote(name) do case unquote(name) do
name when is_atom(name) -> name when is_atom(name) ->
case unquote(term) do case unquote(term) do
%{__struct__: ^name, __exception__: _} -> true %{__struct__: ^name, __exception__: true} -> true
_ -> false _ -> false
end end
@@ -2809,7 +2799,8 @@ defmodule Kernel do
(is_atom(unquote(name)) or :fail) and (is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and :erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term)) :erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end end
end end
end end
@@ -3088,7 +3079,7 @@ defmodule Kernel do
@doc """ @doc """
Pops a key from the given nested structure. Pops a key from the given nested structure.
Uses the `Access` behaviour to traverse the structures Uses the `Access` protocol to traverse the structures
according to the given `keys`, unless the `key` is a according to the given `keys`, unless the `key` is a
function. If the key is a function, it will be invoked function. If the key is a function, it will be invoked
as specified in `get_and_update_in/3`. as specified in `get_and_update_in/3`.
@@ -3533,7 +3524,8 @@ defmodule Kernel do
end end
@doc """ @doc """
A convenience macro that checks if the result of `expression` matches `pattern`. A convenience macro that checks if the right side (an expression) matches the
left side (a pattern).
## Examples ## Examples
@@ -3611,7 +3603,7 @@ defmodule Kernel do
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2} #=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
""" """
defmacro match?(pattern, expression) do defmacro match?(pattern, expr) do
success = success =
quote do quote do
unquote(pattern) -> true unquote(pattern) -> true
@@ -3622,7 +3614,7 @@ defmodule Kernel do
_ -> false _ -> false
end end
{:case, [], [expression, [do: success ++ failure]]} {:case, [], [expr, [do: success ++ failure]]}
end end
@doc """ @doc """
@@ -4052,10 +4044,10 @@ defmodule Kernel do
defp build_if(condition, do: do_clause, else: else_clause) do defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case( annotate_case(
[optimize_boolean: true, type_check: {:case, :if}], [optimize_boolean: true, type_check: :expr],
quote do quote do
case unquote(condition) do case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(else_clause) x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
_ -> unquote(do_clause) _ -> unquote(do_clause)
end end
end end
@@ -4103,15 +4095,9 @@ defmodule Kernel do
end end
defp build_unless(condition, do: do_clause, else: else_clause) do defp build_unless(condition, do: do_clause, else: else_clause) do
annotate_case( quote do
[optimize_boolean: true, type_check: {:case, :unless}], if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
quote do end
case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(do_clause)
_ -> unquote(else_clause)
end
end
)
end end
defp build_unless(_condition, _arguments) do defp build_unless(_condition, _arguments) do
@@ -4379,10 +4365,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "&&") assert_no_match_or_guard_scope(__CALLER__.context, "&&")
annotate_case( annotate_case(
[type_check: {:case, :&&}], [type_check: :expr],
quote do quote do
case unquote(left) do case unquote(left) do
x when unquote(x_is_false_or_nil()) -> x when :"Elixir.Kernel".in(x, [false, nil]) ->
x x
_ -> _ ->
@@ -4422,10 +4408,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "||") assert_no_match_or_guard_scope(__CALLER__.context, "||")
annotate_case( annotate_case(
[type_check: {:case, :||}], [type_check: :expr],
quote do quote do
case unquote(left) do case unquote(left) do
x when unquote(x_is_false_or_nil()) -> x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right) unquote(right)
x -> x ->
@@ -4703,13 +4689,17 @@ defmodule Kernel do
false false
[] -> [] ->
# inlined as false in erlang pass quote do
quote(do: :lists.member(unquote(left), [])) _ = unquote(left)
false
end
[head | tail] = list -> [head | tail] = list ->
case in_body? do # We only expand lists in the body if they are relatively
false -> in_list(left, head, tail, expand, list) # short and it is made only of literal expressions.
true -> quote(do: :lists.member(unquote(left), unquote(right))) case not in_body? or small_literal_list?(right) do
true -> in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
false -> quote(do: :lists.member(unquote(left), unquote(right)))
end end
%{} = right -> %{} = right ->
@@ -4721,7 +4711,7 @@ defmodule Kernel do
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step))) in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step)))
_ when in_body? -> _ when in_body? ->
quote(do: Elixir.Enum.__in__(unquote(left), unquote(right))) quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
_ -> _ ->
raise_on_invalid_args_in_2(right) raise_on_invalid_args_in_2(right)
@@ -4756,6 +4746,12 @@ defmodule Kernel do
end end
end end
defp small_literal_list?(list) when is_list(list) and length(list) <= 32 do
:lists.all(fn x -> is_binary(x) or is_atom(x) or is_number(x) end, list)
end
defp small_literal_list?(_list), do: false
defp in_range(left, first, last, step) when is_integer(step) do defp in_range(left, first, last, step) when is_integer(step) do
in_range_literal(left, first, last, step) in_range_literal(left, first, last, step)
end end
@@ -4763,8 +4759,8 @@ defmodule Kernel do
defp in_range(left, first, last, step) do defp in_range(left, first, last, step) do
quoted = quoted =
quote do quote do
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and :erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
unquote(generated_is_integer(last)) and :erlang.is_integer(unquote(last)) and
((:erlang.>(unquote(step), 0) and ((:erlang.>(unquote(step), 0) and
unquote(increasing_compare(left, first, last))) or unquote(increasing_compare(left, first, last))) or
(:erlang.<(unquote(step), 0) and (:erlang.<(unquote(step), 0) and
@@ -4780,9 +4776,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step > 0 do defp in_range_literal(left, first, last, step) when step > 0 do
quoted = quoted =
quote generated: true do quote do
Kernel.and( :erlang.andalso(
unquote(generated_is_integer(left)), :erlang.is_integer(unquote(left)),
unquote(increasing_compare(left, first, last)) unquote(increasing_compare(left, first, last))
) )
end end
@@ -4792,9 +4788,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step < 0 do defp in_range_literal(left, first, last, step) when step < 0 do
quoted = quoted =
quote generated: true do quote do
Kernel.and( :erlang.andalso(
unquote(generated_is_integer(left)), :erlang.is_integer(unquote(left)),
unquote(decreasing_compare(left, first, last)) unquote(decreasing_compare(left, first, last))
) )
end end
@@ -4808,28 +4804,36 @@ defmodule Kernel do
defp in_range_step(quoted, left, first, step) do defp in_range_step(quoted, left, first, step) do
quote do quote do
Kernel.and( :erlang.andalso(
unquote(quoted), unquote(quoted),
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0) :erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
) )
end end
end end
defp in_list(left, head, tail, expand, right) do defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail]) [head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
:lists.foldl(&quote(do: Kernel.or(unquote(&2), unquote(&1))), head, tail) :lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
end end
defp comp(left, {:|, _, [head, tail]}, expand, right) do defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
case expand.(tail) do case expand.(tail) do
[] -> [] ->
quote(do: :erlang."=:="(unquote(left), unquote(head))) quote(do: :erlang."=:="(unquote(left), unquote(head)))
[tail_head | tail] -> [tail_head | tail] ->
quote do quote do
Kernel.or( :erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)), :erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right)) unquote(in_list(left, tail_head, tail, expand, right, in_body?))
)
end
tail when in_body? ->
quote do
:erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)),
:lists.member(unquote(left), unquote(tail))
) )
end end
@@ -4838,17 +4842,13 @@ defmodule Kernel do
end end
end end
defp comp(left, right, _expand, _right) do defp comp(left, right, _expand, _right, _in_body?) do
quote(do: :erlang."=:="(unquote(left), unquote(right))) quote(do: :erlang."=:="(unquote(left), unquote(right)))
end end
defp generated_is_integer(arg) do
quote generated: true, do: :erlang.is_integer(unquote(arg))
end
defp increasing_compare(var, first, last) do defp increasing_compare(var, first, last) do
quote do quote do
Kernel.and( :erlang.andalso(
:erlang.>=(unquote(var), unquote(first)), :erlang.>=(unquote(var), unquote(first)),
:erlang."=<"(unquote(var), unquote(last)) :erlang."=<"(unquote(var), unquote(last))
) )
@@ -4857,7 +4857,7 @@ defmodule Kernel do
defp decreasing_compare(var, first, last) do defp decreasing_compare(var, first, last) do
quote do quote do
Kernel.and( :erlang.andalso(
:erlang."=<"(unquote(var), unquote(first)), :erlang."=<"(unquote(var), unquote(first)),
:erlang.>=(unquote(var), unquote(last)) :erlang.>=(unquote(var), unquote(last))
) )
@@ -4960,7 +4960,6 @@ defmodule Kernel do
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec binary_slice(binary, integer, non_neg_integer) :: binary
def binary_slice(binary, start, size) def binary_slice(binary, start, size)
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
total = byte_size(binary) total = byte_size(binary)
@@ -5034,7 +5033,6 @@ defmodule Kernel do
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec binary_slice(binary, Range.t()) :: binary
def binary_slice(binary, first..last//step) def binary_slice(binary, first..last//step)
when is_binary(binary) and step > 0 do when is_binary(binary) and step > 0 do
total = byte_size(binary) total = byte_size(binary)
@@ -5149,7 +5147,7 @@ defmodule Kernel do
warning saying that a module has been redefined. warning saying that a module has been redefined.
There are some modules that Elixir does not currently implement but it There are some modules that Elixir does not currently implement but it
may implement in the future. Those modules are reserved and defining may be implement in the future. Those modules are reserved and defining
them will result in a compilation error: them will result in a compilation error:
defmodule Any do defmodule Any do
@@ -5216,11 +5214,6 @@ defmodule Kernel do
end end
end end
defmacro defmodule(alias, [{:do, _block}, {atom, _} | _]) when is_atom(atom) do
raise ArgumentError,
"unexpected reserved word at the top-level of the \"defmodule #{Macro.to_string(alias)}\" do-block: #{atom}"
end
defp module_meta({_, meta, _}), do: meta defp module_meta({_, meta, _}), do: meta
defp module_meta(_), do: [] defp module_meta(_), do: []
@@ -5250,12 +5243,12 @@ defmodule Kernel do
# defmodule Alias nested # defmodule Alias nested
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
module = :elixir_aliases.concat([env.module, h]) module = :elixir_aliases.concat([env.module, h])
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h)) alias = String.to_atom("Elixir." <> Atom.to_string(h))
opts = [as: alias, warn: false] opts = [as: alias, warn: false]
case t do case t do
[] -> {module, module, opts} [] -> {module, module, opts}
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts} _ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
end end
end end
@@ -5562,7 +5555,7 @@ defmodule Kernel do
when the struct is printed: when the struct is printed:
defmodule User do defmodule User do
@derive {Inspect, only: [:name]} @derive {Inspect, only: :name}
defstruct name: nil, age: nil defstruct name: nil, age: nil
end end
@@ -5589,9 +5582,6 @@ defmodule Kernel do
defstruct name: nil, age: 10 + 11 defstruct name: nil, age: 10 + 11
end end
`@enforce_keys` must be set to an atom or a list of unique atoms,
all of which must name fields defined by `defstruct/1`
Now trying to build a struct without the name key will fail: Now trying to build a struct without the name key will fail:
%User{age: 21} %User{age: 21}
@@ -5604,8 +5594,8 @@ defmodule Kernel do
## Types ## Types
It is recommended to define types for structs. By convention, such a type It is recommended to define types for structs. By convention, such a type
is called `t`. To define a type for a struct, the struct literal syntax is is called `t`. To define a struct inside a type, the struct literal syntax
used: is used:
defmodule User do defmodule User do
defstruct name: "John", age: 25 defstruct name: "John", age: 25
@@ -5876,15 +5866,11 @@ defmodule Kernel do
end end
@doc """ @doc """
Defines a custom guard with the given name. Defines a macro suitable for use in guard expressions.
Once defined, custom guards can be invoked within regular code or in It raises at compile time if the `guard` uses expressions that aren't
guards. The module that contains the custom guard must be required before usage. allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
Custom guards are defined by providing a valid guard expression to
the right-hand side of `when`. `defguard` will then expand and validate
the expressions as guards. `defguard` will raise at compile time if the
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
When defining your own guards, consider the When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo) [naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5892,30 +5878,31 @@ defmodule Kernel do
## Example ## Example
For example, to define a guard similar to `Integer.is_even/1`, you can write:
defmodule Integer.Guards do defmodule Integer.Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end end
which can then be used as: defmodule Collatz do
@moduledoc "Tools for working with the Collatz sequence."
import Integer.Guards
require Integer.Guards @doc "Determines the number of steps `n` takes to reach `1`."
Integer.Guards.is_even(3) # If this function never converges, please let me know what `n` you used.
#=> false def converge(n) when n > 0, do: step(n, 0)
## Implementation details defp step(1, step_count) do
step_count
end
Behind the scenes, `defguard` will generate a macro which can be used defp step(n, step_count) when is_even(n) do
inside and outside of guards, preserving their respective semantics. step(div(n, 2), step_count + 1)
end
When invoked inside a guard, it behaves as if the right-hand side of defp step(n, step_count) do
`when` is injected as part of the guard, replacing the custom guard step(3 * n + 1, step_count + 1)
arguments by the expressions given as inputs. end
end
When invoked outside of a guard, it preserves regular function calling
semantics with one caveat: all arguments are evaluated before invocation,
except arguments which are unused, which are then never evaluated.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec defguard(Macro.t()) :: Macro.t() @spec defguard(Macro.t()) :: Macro.t()
@@ -6283,7 +6270,7 @@ defmodule Kernel do
step through the code it sees). For general stepping, you can set breakpoints step through the code it sees). For general stepping, you can set breakpoints
using `IEx.break!/4`. using `IEx.break!/4`.
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints). For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
## Configuring the debug function ## Configuring the debug function
@@ -6330,15 +6317,7 @@ defmodule Kernel do
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
defmacro dbg(code \\ quote(do: binding()), options \\ []) do defmacro dbg(code \\ quote(do: binding()), options \\ []) do
# The compiling process may override the callback by putting it in {mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
# the process dictionary.
dbg_callback =
case :erlang.get({:elixir, :dbg_callback}) do
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
value -> value
end
{mod, fun, args} = dbg_callback
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__) Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end end
@@ -6382,7 +6361,7 @@ defmodule Kernel do
### Passing timeouts ### Passing timeouts
You can also pass timeouts directly to this function, that is, milliseconds or 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. the atom `:infinity`. In this case, this function just returns the given argument.
## Examples ## Examples
@@ -6399,7 +6378,7 @@ defmodule Kernel do
With a timeout: With a timeout:
iex> to_timeout(5_400_000) iex> to_timeout(5400000)
5400000 5400000
iex> to_timeout(:infinity) iex> to_timeout(:infinity)
:infinity :infinity
@@ -6427,20 +6406,12 @@ defmodule Kernel do
{microsecond, _precision} = duration.microsecond {microsecond, _precision} = duration.microsecond
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond) millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
total = duration.week * unquote(week_in_ms) +
duration.week * unquote(week_in_ms) + duration.day * unquote(day_in_ms) +
duration.day * unquote(day_in_ms) + duration.hour * unquote(hour_in_ms) +
duration.hour * unquote(hour_in_ms) + duration.minute * 60_000 +
duration.minute * 60_000 + duration.second * 1000 +
duration.second * 1000 + millisecond
millisecond
if total < 0 do
raise ArgumentError,
"duration must be positive, got: #{inspect(duration)}"
end
total
end end
end end
@@ -6648,13 +6619,11 @@ defmodule Kernel do
defmacro sigil_r(term, modifiers) defmacro sigil_r(term, modifiers)
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1) binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1)
compile_regex(binary, options) compile_regex(binary, options)
end end
defmacro sigil_r({:<<>>, meta, pieces}, options) do defmacro sigil_r({:<<>>, meta, pieces}, options) do
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)} tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
compile_regex(tuple, options) compile_regex(tuple, options)
end end
@@ -6902,7 +6871,7 @@ defmodule Kernel do
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string) defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
when alias >= ?A and alias <= ?Z do when alias >= ?A and alias <= ?Z do
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1) string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
{String.to_unsafe_atom("Elixir." <> last_part), string} {String.to_atom("Elixir." <> last_part), string}
end end
defp maybe_atomize_calendar(_last_part, string) do defp maybe_atomize_calendar(_last_part, string) do
@@ -7013,7 +6982,7 @@ defmodule Kernel do
case mod do case mod do
?s -> parts ?s -> parts
?a -> :lists.map(&String.to_unsafe_atom/1, parts) ?a -> :lists.map(&String.to_atom/1, parts)
?c -> :lists.map(&String.to_charlist/1, parts) ?c -> :lists.map(&String.to_charlist/1, parts)
end end
@@ -7022,7 +6991,7 @@ defmodule Kernel do
case mod do case mod do
?s -> parts ?s -> parts
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts))) ?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts))) ?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
end end
end end
@@ -7056,7 +7025,7 @@ defmodule Kernel do
:guard -> :guard ->
raise ArgumentError, raise ArgumentError,
"invalid expression in guard, #{exp} is not allowed in guards. " <> "invalid expression in guard, #{exp} is not allowed in guards. " <>
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html" "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
_ -> _ ->
:ok :ok
+7 -13
View File
@@ -49,7 +49,7 @@ defmodule Kernel.CLI do
@doc """ @doc """
Runs the given function by catching any failure Runs the given function by catching any failure
and printing them to stderr. `at_exit` hooks are and printing them to stdout. `at_exit` hooks are
also invoked before exiting. also invoked before exiting.
This function is used by Elixir's CLI and also This function is used by Elixir's CLI and also
@@ -99,14 +99,9 @@ defmodule Kernel.CLI do
Shared helper for error formatting on CLI tools. Shared helper for error formatting on CLI tools.
""" """
def format_error(kind, reason, stacktrace) do def format_error(kind, reason, stacktrace) do
{banner, rest} = format_error_parts(kind, reason, stacktrace)
[banner, rest]
end
defp format_error_parts(kind, reason, stacktrace) do
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace) {blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
banner = iodata =
case blamed do case blamed do
%FunctionClauseError{} -> %FunctionClauseError{} ->
formatted = Exception.format_banner(kind, reason, stacktrace) formatted = Exception.format_banner(kind, reason, stacktrace)
@@ -117,7 +112,7 @@ defmodule Kernel.CLI do
Exception.format_banner(kind, blamed, stacktrace) Exception.format_banner(kind, blamed, stacktrace)
end end
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]} [iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
end end
@doc """ @doc """
@@ -184,8 +179,7 @@ defmodule Kernel.CLI do
## Error handling ## Error handling
defp print_error(kind, reason, stacktrace) do defp print_error(kind, reason, stacktrace) do
{banner, rest} = format_error_parts(kind, reason, stacktrace) IO.write(:stderr, format_error(kind, reason, stacktrace))
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
end end
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node) defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
@@ -206,7 +200,7 @@ defmodule Kernel.CLI do
end end
@elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++ @elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++
[:elixir_dispatch, :elixir_expand, :elixir_lexical] ++ [:elixir_def, :elixir_dispatch, :elixir_expand, :elixir_lexical] ++
[:elixir_map, :elixir_module] ++ [:elixir_map, :elixir_module] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++ [:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker] [Kernel.ErrorHandler, Module.ParallelChecker]
@@ -344,7 +338,7 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | verbose_compile: true}) parse_argv(t, %{config | verbose_compile: true})
end end
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | profile: :time}) parse_argv(t, %{config | profile: :time})
end end
@@ -433,7 +427,7 @@ defmodule Kernel.CLI do
end end
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
node = List.to_unsafe_atom(node) node = List.to_atom(node)
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined. # Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
_ = :net_kernel.connect_node(node) _ = :net_kernel.connect_node(node)
+2 -38
View File
@@ -21,18 +21,9 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout) :gen_server.call(pid, :references, @timeout)
end end
@doc """
Invoked during module expansion to annotate a require
that must be warned if unused.
"""
def warn_require(pid, meta, module, alias) do
:gen_server.cast(pid, {:warn_require, module, meta, alias})
module
end
@doc """ @doc """
Invoked during module expansion to annotate an alias Invoked during module expansion to annotate an alias
that must be warned if unused. must be warned if unused.
""" """
def warn_alias(pid, meta, alias, module) do def warn_alias(pid, meta, alias, module) do
:gen_server.cast(pid, {:warn_alias, alias, meta}) :gen_server.cast(pid, {:warn_alias, alias, meta})
@@ -41,7 +32,7 @@ defmodule Kernel.LexicalTracker do
@doc """ @doc """
Invoked during module expansion to annotate an import Invoked during module expansion to annotate an import
that must be warned if unused. must be warned if unused.
""" """
def warn_import(pid, module) do def warn_import(pid, module) do
:gen_server.cast(pid, {:warn_import, module}) :gen_server.cast(pid, {:warn_import, module})
@@ -66,11 +57,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_export, module}) :gen_server.cast(pid, {:add_export, module})
end end
@doc false
def add_require(pid, module, meta) when is_atom(module) do
:gen_server.cast(pid, {:add_require, module, meta})
end
@doc false @doc false
def add_import(pid, module, fas, meta, warn) when is_atom(module) do def add_import(pid, module, fas, meta, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_import, module, fas, meta, warn}) :gen_server.cast(pid, {:add_import, module, fas, meta, warn})
@@ -133,18 +119,12 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :unused_aliases, @timeout) :gen_server.call(pid, :unused_aliases, @timeout)
end end
@doc false
def collect_unused_requires(pid) do
:gen_server.call(pid, :unused_requires, @timeout)
end
# Callbacks # Callbacks
def init(:ok) do def init(:ok) do
state = %{ state = %{
aliases: %{}, aliases: %{},
imports: %{}, imports: %{},
requires: %{},
references: %{}, references: %{},
exports: %{}, exports: %{},
cache: %{}, cache: %{},
@@ -170,18 +150,6 @@ defmodule Kernel.LexicalTracker do
{:reply, Enum.sort(imports), state} {:reply, Enum.sort(imports), state}
end end
def handle_call(:unused_requires, _from, state) do
%{references: references, aliases: aliases} = state
unused_requires =
for {module, {meta, alias}} <- state.requires,
Map.get(references, module) != :compile do
{module, meta, alias, Map.get(aliases, alias) == :used}
end
{:reply, Enum.sort(unused_requires), state}
end
def handle_call(:references, _from, state) do def handle_call(:references, _from, state) do
{compile, runtime} = partition(Map.to_list(state.references), [], []) {compile, runtime} = partition(Map.to_list(state.references), [], [])
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state} {:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
@@ -277,10 +245,6 @@ defmodule Kernel.LexicalTracker do
{:noreply, put_in(state.imports[module][@warn_key], true)} {:noreply, put_in(state.imports[module][@warn_key], true)}
end end
def handle_cast({:warn_require, module, meta, alias}, state) do
{:noreply, put_in(state.requires[module], {meta, alias})}
end
@doc false @doc false
def handle_info(_msg, state) do def handle_info(_msg, state) do
{:noreply, state} {:noreply, state}
+26 -46
View File
@@ -25,8 +25,9 @@ defmodule Kernel.ParallelCompiler do
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()), each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()), each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()), each_module: (Path.t(), module(), binary() -> term()),
each_cycle: (-> {:compile, [Path.t()], [Code.diagnostic(:warning)]} each_cycle: ([module()], [Code.diagnostic(:warning)] ->
| {:runtime, [{module(), Path.t()}], [Code.diagnostic(:warning)]}), {:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(), long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(), long_verification_threshold: pos_integer(),
verification: boolean(), verification: boolean(),
@@ -34,8 +35,7 @@ defmodule Kernel.ParallelCompiler do
dest: Path.t(), dest: Path.t(),
beam_timestamp: term(), beam_timestamp: term(),
return_diagnostics: boolean(), return_diagnostics: boolean(),
max_concurrency: pos_integer(), max_concurrency: pos_integer()
purge_compiler_modules: boolean()
] ]
@typedoc """ @typedoc """
@@ -51,8 +51,8 @@ defmodule Kernel.ParallelCompiler do
@doc """ @doc """
Starts a task for parallel compilation. Starts a task for parallel compilation.
""" """
# TODO: Remove me on Elixir 2.0 # TODO: Deprecate this on Elixir v1.20.
@deprecated "Use `pmap/2` instead" @doc deprecated: "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun) {ref, task} = inner_async(fun)
send(task.pid, ref) send(task.pid, ref)
@@ -100,8 +100,6 @@ defmodule Kernel.ParallelCompiler do
This function allows a developer to perform such tasks. This function allows a developer to perform such tasks.
""" """
@doc since: "1.16.0" @doc since: "1.16.0"
@spec pmap(Enumerable.t(input), (input -> output)) :: [output]
when input: term, output: term
def pmap(collection, fun) when is_function(fun, 1) do def pmap(collection, fun) when is_function(fun, 1) do
ref = make_ref() ref = make_ref()
@@ -173,25 +171,25 @@ defmodule Kernel.ParallelCompiler do
* `:each_long_verification` (since v1.19.0) - for each file that takes more * `:each_long_verification` (since v1.19.0) - for each file that takes more
than a given timeout (see the `:long_verification_threshold` option) to than a given timeout (see the `:long_verification_threshold` option) to
verify, invoke this callback passing the module as its argument (and compile, invoke this callback passing the module as its argument (and
optionally the PID of the process verifying the module) optionally the PID of the process verifying the module)
* `:each_module` - for each module compiled, invokes the callback passing * `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode the file, module and the module bytecode
* `:each_cycle` - invoked after each compilation cycle and should return one * `:each_cycle` - after the given files are compiled, invokes this function
of the following values: that should return the following values:
* `{:compile, modules, warnings}` - to continue compilation with a list of * `{:compile, modules, warnings}` - to continue compilation with a list of
further module files to compile further modules to compile
* `{:runtime, modules, warnings}` - to stop compilation and verify the list * `{:runtime, modules, warnings}` - to stop compilation and verify the list
of `{module, path}` pairs because dependent modules have changed of modules because dependent modules have changed
* `:long_compilation_threshold` - the timeout (in seconds) to check for files * `:long_compilation_threshold` - the timeout (in seconds) to check for files
taking too long to compile. For each file that exceeds the threshold, the taking too long to compile. For each file that exceeds the threshold, the
`:each_long_compilation` callback is invoked. Defaults to `10` seconds. `:each_long_compilation` callback is invoked. Defaults to `10` seconds.
* `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to * `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to
check for modules taking too long to verify. For each module that exceeds the check for modules taking too long to compile. For each module that exceeds the
threshold, the `:each_long_verification` callback is invoked. Defaults to threshold, the `:each_long_verification` callback is invoked. Defaults to
`10` seconds. `10` seconds.
@@ -199,11 +197,8 @@ defmodule Kernel.ParallelCompiler do
deprecation warnings, and type checking should run. Defaults to `true`. deprecation warnings, and type checking should run. Defaults to `true`.
We recommend disabling it only for debugging purposes. We recommend disabling it only for debugging purposes.
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle, * `:profile` - if set to `:time` measure the compilation time of each compilation cycle
each module type check, and group pass checker and group pass checker
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
after compilation (see `Code.purge_compiler_modules/0`)
* `:dest` - the destination directory for the BEAM files. When using `compile/2`, * `:dest` - the destination directory for the BEAM files. When using `compile/2`,
this information is only used to properly annotate the BEAM files before this information is only used to properly annotate the BEAM files before
@@ -214,7 +209,7 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of * `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples. a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backward compatibility reasons. This option must be set to true, except for backwards compatibibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel. * `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially. Setting this option to 1 will compile files sequentially.
@@ -271,7 +266,7 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of * `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples. a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backward compatibility reasons. This option must be set to true, except for backwards compatibibility reasons.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@@ -341,14 +336,7 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(schedulers, checker, files, output, options) do defp spawn_workers(schedulers, checker, files, output, options) do
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000 threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = :erlang.send_after(threshold, self(), :threshold_check) timer_ref = Process.send_after(self(), :threshold_check, threshold)
purge_compiler_modules =
if Keyword.get(options, :purge_compiler_modules, false) do
fn -> :elixir_code_server.cast(:purge_compiler_modules) end
else
fn -> :ok end
end
{outcome, state} = {outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{ spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
@@ -365,8 +353,7 @@ defmodule Kernel.ParallelCompiler do
long_compilation_threshold: threshold, long_compilation_threshold: threshold,
schedulers: schedulers, schedulers: schedulers,
checker: checker, checker: checker,
verification?: Keyword.get(options, :verification, true), verification?: Keyword.get(options, :verification, true)
purge_compiler_modules: purge_compiler_modules
}) })
Process.cancel_timer(state.timer_ref) Process.cancel_timer(state.timer_ref)
@@ -461,7 +448,7 @@ defmodule Kernel.ParallelCompiler do
modules = write_module_binaries(result, state.output, state) modules = write_module_binaries(result, state.output, state)
profile(state, "after compile callback", state.after_compile) profile(state, "after compile callback", state.after_compile)
{runtime_warnings, errors} = runtime_warnings =
if state.verification? do if state.verification? do
profile( profile(
state, state,
@@ -472,19 +459,11 @@ defmodule Kernel.ParallelCompiler do
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
) )
else else
{[], []} []
end end
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings} info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, modules, info}, state}
case errors do
[] ->
{{:ok, modules, info}, state}
_ ->
IO.puts(:stderr, "== Type checking failed with errors ==")
{{:error, errors, info}, state}
end
end end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0} defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
@@ -591,11 +570,11 @@ defmodule Kernel.ParallelCompiler do
case cycle_return do case cycle_return do
{:runtime, dependent_modules, extra_warnings} -> {:runtime, dependent_modules, extra_warnings} ->
state.purge_compiler_modules.() :elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state) verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, [], extra_warnings} -> {:compile, [], extra_warnings} ->
state.purge_compiler_modules.() :elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, [], state) verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} -> {:compile, more, extra_warnings} ->
@@ -842,7 +821,7 @@ defmodule Kernel.ParallelCompiler do
end end
end end
timer_ref = :erlang.send_after(state.long_compilation_threshold, self(), :threshold_check) timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
state = %{state | timer_ref: timer_ref} state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
@@ -900,7 +879,8 @@ defmodule Kernel.ParallelCompiler do
end end
defp return_error(warnings, errors, state, fun) do defp return_error(warnings, errors, state, fun) do
state.purge_compiler_modules.() # Also prune compiler modules in case of errors
:elixir_code_server.cast(:purge_compiler_modules)
errors = errors =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} -> Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
+41 -29
View File
@@ -768,8 +768,8 @@ defmodule Kernel.SpecialForms do
It is used in typespecs to specify the type of a variable, It is used in typespecs to specify the type of a variable,
function or of a type itself: function or of a type itself:
@type num :: integer | float @type number :: integer | float
@spec add(num, num) :: num @spec add(number, number) :: number
It may also be used in bit strings to specify the type It may also be used in bit strings to specify the type
of a given bit segment: of a given bit segment:
@@ -1333,9 +1333,9 @@ defmodule Kernel.SpecialForms do
sum(1, value, 3) sum(1, value, 3)
end end
However, the code above does not work as expected, because this
injects the representation of the `value` variable, Which the argument for the `:sum` function call is not the
not its contents: expected result:
{:sum, [], [1, {:value, [], Elixir}, 3]} {:sum, [], [1, {:value, [], Elixir}, 3]}
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
Let's give it a try on IEx: Let's give it a try on IEx:
iex> opts = %{"width" => 10, "height" => 15} iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, "width"), iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, "height") do ...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height} ...> {:ok, width * height}
...> end ...> end
{:ok, 150} {:ok, 150}
@@ -1603,13 +1603,21 @@ defmodule Kernel.SpecialForms do
If all clauses match, the `do` block is executed, returning its result. If all clauses match, the `do` block is executed, returning its result.
Otherwise the chain is aborted and the non-matched value is returned: Otherwise the chain is aborted and the non-matched value is returned:
iex> opts = %{"width" => 10} iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, "width"), iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, "height") do ...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height} ...> {:ok, width * height}
...> end ...> end
:error :error
Guards can be used in patterns as well:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"}
As in `for/1`, variables bound inside `with/1` won't be accessible As in `for/1`, variables bound inside `with/1` won't be accessible
outside of `with/1`. outside of `with/1`.
@@ -1653,18 +1661,22 @@ defmodule Kernel.SpecialForms do
An `else` option can be given to modify what is being returned from An `else` option can be given to modify what is being returned from
`with` in the case of a failed match: `with` in the case of a failed match:
with {:ok, content} <- File.read(path), iex> opts = %{width: 10}
:ok <- File.write(path, [content, "!"]) do iex> with {:ok, width} <- Map.fetch(opts, :width),
:ok ...> {:ok, height} <- Map.fetch(opts, :height) do
else ...> {:ok, width * height}
{:error, reason} -> ...> else
Logger.error("could not append ! to \#{path} with reason: \#{reason}") ...> :error ->
:error ...> {:error, :wrong_data}
end ...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
The `else` block works like a `case`: it can have multiple clauses, The `else` block works like a `case`: it can have multiple clauses,
and the first match will be used. Variables bound inside `with` and the first match will be used. Variables bound inside `with` (such as
(such as `content` in this example) are not available in the `else` block. `width` in this example) are not available in the `else` block.
If an `else` block is used and there are no matching clauses, a `WithClauseError` If an `else` block is used and there are no matching clauses, a `WithClauseError`
exception is raised. exception is raised.
@@ -1975,13 +1987,13 @@ defmodule Kernel.SpecialForms do
While it is not possible to match against multiple patterns in a single While it is not possible to match against multiple patterns in a single
clause, it's possible to match against multiple values by using guards: clause, it's possible to match against multiple values by using guards:
iex> case 2 do iex> case :two do
...> value when value in [1, 2] -> ...> value when value in [:one, :two] ->
...> "#{value} has been matched" ...> "#{value} has been matched"
...> 3 -> ...> :three ->
...> "3 has been matched" ...> "three has been matched"
...> end ...> end
"2 has been matched" "two has been matched"
""" """
defmacro case(condition, clauses), do: error!([condition, clauses]) defmacro case(condition, clauses), do: error!([condition, clauses])
@@ -2077,9 +2089,9 @@ defmodule Kernel.SpecialForms do
iex> try do iex> try do
...> 1 / 0 ...> 1 / 0
...> rescue ...> rescue
...> x in [ArithmeticError] -> {:rescued, Exception.message(x)} ...> x in [ArithmeticError] -> [:rescued, is_exception(x)]
...> end ...> end
{:rescued, "bad argument in arithmetic expression"} [:rescued, true]
Rescue different errors with separate clauses: Rescue different errors with separate clauses:
@@ -2343,7 +2355,7 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args]) defmacro try(args), do: error!([args])
@doc """ @doc """
Consumes the first message matching any of the given clauses in the current Checks if there is a message matching any of the given clauses in the current
process mailbox. process mailbox.
If there is no matching message, the current process waits until a matching If there is no matching message, the current process waits until a matching
+15 -36
View File
@@ -255,20 +255,9 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do case type_to_signature(expr) do
{name, arity} = type_pair -> {name, arity} = type_pair ->
cond do if built_in_type?(name, arity) do
# This is a built-in type since OTP 29 but it just generates a warning for now message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
{name, arity} == {:record, 0} -> compile_error(env, message)
IO.warn("type #{name}/#{arity} is overriding a built-in type",
file: file,
line: line
)
built_in_type?(name, arity) ->
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
compile_error(env, message)
true ->
:ok
end end
if Map.has_key?(type_pairs, type_pair) do if Map.has_key?(type_pairs, type_pair) do
@@ -610,7 +599,8 @@ defmodule Kernel.Typespec do
types = types =
:lists.map( :lists.map(
fn %{field: field} -> fn %{field: field} ->
{field, Keyword.get(fields, field, quote(do: term()))} default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
end, end,
struct_info struct_info
) )
@@ -686,13 +676,6 @@ defmodule Kernel.Typespec do
{{:type, location(meta), :range, [left, right]}, state} {{:type, location(meta), :range, [left, right]}, state}
end end
defp typespec({:..//, _meta, [_first, _last, _step]} = range, _vars, caller, _state) do
compile_error(
caller,
"ranges with steps are not supported in typespecs, got: #{Macro.to_string(range)}"
)
end
# Handle special forms # Handle special forms
defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do
typespec(caller.module, vars, caller, state) typespec(caller.module, vars, caller, state)
@@ -867,29 +850,29 @@ defmodule Kernel.Typespec do
{{:type, location(meta), :nonempty_string, args}, state} {{:type, location(meta), :nonempty_string, args}, state}
end end
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
if type == :char_list do if type == :char_list do
warning = "the char_list() type is deprecated, use charlist()" warning = "the char_list() type is deprecated, use charlist()"
IO.warn(warning, caller) IO.warn(warning, caller)
end end
remote_typespec(:charlist, meta, [], vars, caller, state) typespec(quote(do: :elixir.charlist()), vars, caller, state)
end end
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state) typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
end end
defp typespec({:struct, meta, []}, vars, caller, state) do defp typespec({:struct, _meta, []}, vars, caller, state) do
remote_typespec(:struct, meta, [], vars, caller, state) typespec(quote(do: :elixir.struct()), vars, caller, state)
end end
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
remote_typespec(:as_boolean, meta, [arg], vars, caller, state) typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
end end
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
remote_typespec(:keyword, meta, args, vars, caller, state) typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
end end
defp typespec({:fun, meta, args}, vars, caller, state) do defp typespec({:fun, meta, args}, vars, caller, state) do
@@ -1014,10 +997,6 @@ defmodule Kernel.Typespec do
{{:remote_type, location(meta), [remote, name, args]}, state} {{:remote_type, location(meta), [remote, name, args]}, state}
end end
defp remote_typespec(name, meta, args, vars, caller, state) do
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
end
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)] defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
defp collect_union(v), do: [v] defp collect_union(v), do: [v]
+22 -19
View File
@@ -162,8 +162,16 @@ defmodule Kernel.Utils do
# TODO: Make it raise on v2.0 # TODO: Make it raise on v2.0
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env) warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
field_map = :maps.from_list(fields) foreach = fn
struct = :maps.put(:__struct__, module, field_map) key when is_atom(key) ->
:ok
key ->
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false) escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body = body =
@@ -207,7 +215,7 @@ defmodule Kernel.Utils do
end end
end end
case enforce_keys -- :maps.keys(field_map) do case enforce_keys -- :maps.keys(struct) do
[] -> [] ->
mapper = fn {key, val} -> mapper = fn {key, val} ->
%{field: key, default: val, required: :lists.member(key, enforce_keys)} %{field: key, default: val, required: :lists.member(key, enforce_keys)}
@@ -217,9 +225,10 @@ defmodule Kernel.Utils do
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive})) derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body} {struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
invalid_keys -> error_keys ->
raise ArgumentError, raise ArgumentError,
"unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}" "@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
"#{inspect(fields)}"
end end
end end
@@ -257,7 +266,7 @@ defmodule Kernel.Utils do
module.exception([]) module.exception([])
end end
def raise(%_{__exception__: _} = exception) do def raise(%_{__exception__: true} = exception) do
exception exception
end end
@@ -320,27 +329,21 @@ defmodule Kernel.Utils do
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t() @spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
def defguard(args, expr, env) do def defguard(args, expr, env) do
{_, vars} = extract_refs_from_args(args) {^args, vars} = extract_refs_from_args(args)
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars) env = :elixir_env.with_vars(%{env | context: :guard}, vars)
body_expr = expand_defguard(expr, %{env | context: nil}, vars) {expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
quote do quote do
case Macro.Env.in_guard?(__CALLER__) do case Macro.Env.in_guard?(__CALLER__) do
true -> true ->
unquote(literal_quote(unquote_every_ref(guard_expr, vars), [])) unquote(literal_quote(unquote_every_ref(expr, vars), []))
false -> false ->
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true)) unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
end end
end end
end end
defp expand_defguard(expr, env, vars) do
env = :elixir_env.with_vars(env, vars)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
expr
end
defp extract_refs_from_args(args) do defp extract_refs_from_args(args) do
Macro.postwalk(args, [], fn Macro.postwalk(args, [], fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) -> {ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -366,7 +369,7 @@ defmodule Kernel.Utils do
end end
# Prefaces `guard` with unquoted versions of `refs`. # Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, %{module: module}) do defp unquote_refs_once(guard, refs, module) do
{guard, used_refs} = {guard, used_refs} =
Macro.postwalk(guard, %{}, fn Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) -> {ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -379,7 +382,7 @@ defmodule Kernel.Utils do
{new_var, acc} {new_var, acc}
%{} -> %{} ->
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1)) generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
new_var = Macro.unique_var(generated, module) new_var = Macro.unique_var(generated, module)
{new_var, Map.put(acc, pair, {new_var, var})} {new_var, Map.put(acc, pair, {new_var, var})}
end end
+44 -77
View File
@@ -38,7 +38,7 @@ defmodule Keyword do
## Duplicate keys and ordering ## Duplicate keys and ordering
A keyword list may have duplicate keys so it is not strictly a key-value A keyword may have duplicate keys so it is not strictly a key-value
data type. However, most of the functions in this module work on a data type. However, most of the functions in this module work on a
key-value structure and behave similar to the functions you would key-value structure and behave similar to the functions you would
find in the `Map` module. For example, `Keyword.get/3` will get the first find in the `Map` module. For example, `Keyword.get/3` will get the first
@@ -119,23 +119,14 @@ defmodule Keyword do
iex> Keyword.from_keys([:foo, :bar, :baz], :atom) iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
[foo: :atom, bar: :atom, baz: :atom] [foo: :atom, bar: :atom, baz: :atom]
iex> Keyword.from_keys([], :atom) iex> Keyword.from_keys([], :atom)
[] []
iex> Keyword.from_keys(["foo"], :bar)
** (ArgumentError) expected a list of atoms as keys, got: "foo"
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec from_keys([key], value) :: t(value) @spec from_keys([key], value) :: t(value)
def from_keys(keys, value) when is_list(keys) do def from_keys(keys, value) when is_list(keys) do
:lists.map( :lists.map(&{&1, value}, keys)
fn
key when is_atom(key) -> {key, value}
other -> raise ArgumentError, "expected a list of atoms as keys, got: #{inspect(other)}"
end,
keys
)
end end
@doc """ @doc """
@@ -269,60 +260,38 @@ defmodule Keyword do
@doc since: "1.13.0" @doc since: "1.13.0"
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) :: @spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
{:ok, keyword()} | {:error, [atom]} {:ok, keyword()} | {:error, [atom]}
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
def validate(keyword, values) when is_list(keyword) and is_list(values) do def validate(keyword, values) when is_list(keyword) and is_list(values) do
validate_merge(keyword, values, [], keyword) validate(keyword, values, [], [], [])
end end
defp validate_merge([], values, values_pre, original), defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))} case find_key!(key, values1, values2) do
{values1, values2} ->
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original) validate(keyword, values1, values2, [pair | acc], bad_keys)
when is_atom(key) do
case head do
^key -> validate_merge(keyword, tail, values_pre, original)
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
end
end
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
case find_key!(key, values_pre, []) do
{new_values, new_values_pre} ->
validate_merge(keyword, new_values, new_values_pre, original)
:error -> :error ->
validate_fallback(keyword, values_pre, [key]) case find_key!(key, values2, values1) do
{values1, values2} ->
validate(keyword, values1, values2, [pair | acc], bad_keys)
:error ->
validate(keyword, values1, values2, acc, [key | bad_keys])
end
end end
end end
defp validate_merge([pair | _], _, _, _), defp validate([], values1, values2, acc, []) do
do: {:ok, move_pairs!(values1, move_pairs!(values2, acc))}
raise(
ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
)
defp validate_fallback([{key, _} | keyword], values, bad_keys)
when is_atom(key) do
case find_key!(key, values, []) do
{rest, acc} ->
validate_fallback(keyword, rest ++ acc, bad_keys)
:error ->
validate_fallback(keyword, values, [key | bad_keys])
end
end end
defp validate_fallback([], _, bad), do: {:error, bad} defp validate([], _values1, _values2, _acc, bad_keys) do
{:error, bad_keys}
end
defp validate_fallback([p | _], _, _), defp validate([pair | _], _values1, _values2, _acc, []) do
do: raise ArgumentError,
raise( "expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
ArgumentError, end
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
)
defp find_key!(key, [key | rest], acc), do: {rest, acc} defp find_key!(key, [key | rest], acc), do: {rest, acc}
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc} defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
@@ -468,7 +437,7 @@ defmodule Keyword do
end end
@doc """ @doc """
Gets the value for `key` and updates it in one pass, deleting duplicate keys. Gets the value from `key` and updates it, all in one pass.
The `fun` argument receives the value of `key` (or `nil` if `key` The `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the current value is not present) and must return a two-element tuple: the current value
@@ -514,10 +483,10 @@ defmodule Keyword do
defp get_and_update([{key, current} | t], acc, key, fun) do defp get_and_update([{key, current} | t], acc, key, fun) do
case fun.(current) do case fun.(current) do
{get, value} -> {get, value} ->
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])} {get, :lists.reverse(acc, [{key, value} | t])}
:pop -> :pop ->
{current, :lists.reverse(acc, delete(t, key))} {current, :lists.reverse(acc, t)}
other -> other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}" raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
@@ -540,8 +509,7 @@ defmodule Keyword do
end end
@doc """ @doc """
Gets the value for `key` and updates it in one pass, deleting duplicate keys, Gets the value under `key` and updates it. Raises if there is no `key`.
raising if `key` can't be found in `keywords`.
The `fun` argument receives the value under `key` and must return a The `fun` argument receives the value under `key` and must return a
two-element tuple: the current value (the retrieved value, which can be two-element tuple: the current value (the retrieved value, which can be
@@ -577,21 +545,21 @@ defmodule Keyword do
get_and_update!(keywords, key, fun, []) get_and_update!(keywords, key, fun, [])
end end
defp get_and_update!([{key, value} | t], key, fun, acc) do defp get_and_update!([{key, value} | keywords], key, fun, acc) do
case fun.(value) do case fun.(value) do
{get, value} -> {get, value} ->
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])} {get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
:pop -> :pop ->
{value, :lists.reverse(acc, delete(t, key))} {value, :lists.reverse(acc, keywords)}
other -> other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}" raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end end
end end
defp get_and_update!([{_, _} = h | t], key, fun, acc) do defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
get_and_update!(t, key, fun, [h | acc]) get_and_update!(keywords, key, fun, [e | acc])
end end
defp get_and_update!([], key, _fun, acc) when is_atom(key) do defp get_and_update!([], key, _fun, acc) when is_atom(key) do
@@ -987,7 +955,7 @@ defmodule Keyword do
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1]) iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
true true
Comparison between values is done with `===/2`, Comparison between values is done with `===/3`,
which means integers are not equivalent to floats: which means integers are not equivalent to floats:
iex> Keyword.equal?([a: 1.0], [a: 1]) iex> Keyword.equal?([a: 1.0], [a: 1])
@@ -1323,8 +1291,7 @@ defmodule Keyword do
@doc """ @doc """
Drops the given `keys` from the keyword list. Drops the given `keys` from the keyword list.
If a key occurs multiple times, all of its entries are removed when that key Removes duplicate keys from the new keyword list.
is included in `keys`.
## Examples ## Examples
@@ -1365,9 +1332,9 @@ defmodule Keyword do
""" """
@spec pop(t, key, default) :: {value | default, t} @spec pop(t, key, default) :: {value | default, t}
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do case fetch(keywords, key) do
{^key, value} -> {value, delete_key(keywords, key)} {:ok, value} -> {value, delete(keywords, key)}
false -> {default, keywords} :error -> {default, keywords}
end end
end end
@@ -1392,9 +1359,9 @@ defmodule Keyword do
@doc since: "1.10.0" @doc since: "1.10.0"
@spec pop!(t, key) :: {value, t} @spec pop!(t, key) :: {value, t}
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do case fetch(keywords, key) do
{^key, value} -> {value, delete_key(keywords, key)} {:ok, value} -> {value, delete(keywords, key)}
false -> raise KeyError, key: key, term: keywords :error -> raise KeyError, key: key, term: keywords
end end
end end
@@ -1434,7 +1401,7 @@ defmodule Keyword do
do: {values, acc} do: {values, acc}
@doc """ @doc """
Lazily returns the first value for `key` and removes all associated entries in the keyword list. Lazily returns and removes all values associated with `key` in the keyword list.
This is useful if the default value is very expensive to calculate or This is useful if the default value is very expensive to calculate or
generally difficult to set up and tear down again. generally difficult to set up and tear down again.
@@ -1457,9 +1424,9 @@ defmodule Keyword do
@spec pop_lazy(t, key, (-> value)) :: {value, t} @spec pop_lazy(t, key, (-> value)) :: {value, t}
def pop_lazy(keywords, key, fun) def pop_lazy(keywords, key, fun)
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
case :lists.keyfind(key, 1, keywords) do case fetch(keywords, key) do
{^key, value} -> {value, delete_key(keywords, key)} {:ok, value} -> {value, delete(keywords, key)}
false -> {fun.(), keywords} :error -> {fun.(), keywords}
end end
end end
+26 -141
View File
@@ -102,7 +102,7 @@ defmodule List do
Even though the representation changed, the raw data does remain a list of Even though the representation changed, the raw data does remain a list of
integers, which can be handled as such: integers, which can be handled as such:
iex> inspect(~c"abc", charlists: :as_lists) iex> inspect(~c"abc", charlists: :as_list)
"[97, 98, 99]" "[97, 98, 99]"
iex> Enum.map(~c"abc", fn num -> 1000 + num end) iex> Enum.map(~c"abc", fn num -> 1000 + num end)
[1097, 1098, 1099] [1097, 1098, 1099]
@@ -187,10 +187,9 @@ defmodule List do
""" """
@spec duplicate(any, 0) :: [] @spec duplicate(any, 0) :: []
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var @spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, []) def duplicate(elem, n) do
:lists.duplicate(n, elem)
defp duplicate(0, _elem, acc), do: acc end
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
@doc """ @doc """
Flattens the given `list` of nested lists. Flattens the given `list` of nested lists.
@@ -298,30 +297,6 @@ defmodule List do
def first([], default), do: default def first([], default), do: default
def first([head | _], _default), do: head def first([head | _], _default), do: head
@doc """
Returns the first element in `list`.
If `list` is empty, an error is raised.
## Examples
iex> List.first!([1])
1
iex> List.first!([1, 2, 3])
1
"""
@doc since: "1.20.0"
@spec first!([elem, ...]) :: elem when elem: var
def first!(list)
def first!([head | _]), do: head
def first!([]) do
raise ArgumentError, "attempted to get the first element of an empty list"
end
@doc """ @doc """
Returns the last element in `list` or `default` if `list` is empty. Returns the last element in `list` or `default` if `list` is empty.
@@ -350,31 +325,6 @@ defmodule List do
def last([head], _default), do: head def last([head], _default), do: head
def last([_ | tail], default), do: last(tail, default) def last([_ | tail], default), do: last(tail, default)
@doc """
Returns the last element in `list`.
If `list` is empty, an error is raised.
## Examples
iex> List.last!([1])
1
iex> List.last!([1, 2, 3])
3
"""
@doc since: "1.20.0"
@spec last!([elem, ...]) :: elem when elem: var
def last!(list)
def last!([head]), do: head
def last!([_ | tail]), do: last!(tail)
def last!([]) do
raise ArgumentError, "attempted to get the last element of an empty list"
end
@doc """ @doc """
Receives a list of tuples and returns the first tuple Receives a list of tuples and returns the first tuple
where the element at `position` in the tuple matches the where the element at `position` in the tuple matches the
@@ -429,7 +379,7 @@ defmodule List do
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec keyfind!([tuple], any, non_neg_integer) :: tuple @spec keyfind!([tuple], any, non_neg_integer) :: any
def keyfind!(list, key, position) when is_integer(position) do def keyfind!(list, key, position) when is_integer(position) do
:lists.keyfind(key, position + 1, list) || :lists.keyfind(key, position + 1, list) ||
raise KeyError, raise KeyError,
@@ -514,8 +464,8 @@ defmodule List do
As in `Enum.sort/2`, avoid using the default sorting function to sort As in `Enum.sort/2`, avoid using the default sorting function to sort
structs, as by default it performs structural comparison instead of a structs, as by default it performs structural comparison instead of a
semantic one. In such cases, you shall pass a sorting function as the third semantic one. In such cases, you shall pass a sorting function as third
argument or any module that implements a `compare/2` function. For example, element or any module that implements a `compare/2` function. For example,
if you have tuples with user names and their birthday, and you want to if you have tuples with user names and their birthday, and you want to
sort on their birthday, in both ascending and descending order, you should sort on their birthday, in both ascending and descending order, you should
do: do:
@@ -655,7 +605,7 @@ defmodule List do
end end
@doc """ @doc """
Wraps `term` in a list if it is not a list. Wraps `term` in a list if this is not list.
If `term` is already a list, it returns the list. If `term` is already a list, it returns the list.
If `term` is `nil`, it returns an empty list. If `term` is `nil`, it returns an empty list.
@@ -954,12 +904,9 @@ defmodule List do
@spec pop_at(list, integer, any) :: {any, list} @spec pop_at(list, integer, any) :: {any, list}
def pop_at(list, index, default \\ nil) when is_integer(index) do def pop_at(list, index, default \\ nil) when is_integer(index) do
if index < 0 do if index < 0 do
case length(list) + index do do_pop_at(list, length(list) + index, default, [])
index when index < 0 -> {default, list}
index -> do_pop_at(list, index, default, [], list)
end
else else
do_pop_at(list, index, default, [], list) do_pop_at(list, index, default, [])
end end
end end
@@ -1021,51 +968,31 @@ defmodule List do
:lists.suffix(suffix, list) :lists.suffix(suffix, list)
end end
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
def to_atom(charlist) do
:erlang.list_to_atom(charlist)
end
@doc """ @doc """
Converts a charlist to an existing atom or creates a new one. Converts a charlist to an atom.
Elixir supports conversions from charlists which contain any Unicode Elixir supports conversions from charlists which contain any Unicode
code point. code point.
> #### Dynamic Atom Creation {: .warning}
>
> This function creates atoms dynamically and atoms are
> not garbage-collected. Therefore, `charlist` should not be an
> untrusted value, such as input received from a socket or during
> a web request. Consider using `to_existing_atom/1` instead.
By default, the maximum number of atoms is `1_048_576`. This limit
can be raised or lowered using the VM option `+t`.
The maximum atom size is 255 Unicode code points.
Inlined by the compiler. Inlined by the compiler.
## Examples ## Examples
iex> List.to_unsafe_atom(~c"Elixir") iex> List.to_atom(~c"Elixir")
:Elixir :Elixir
iex> List.to_unsafe_atom(~c"🌢 Elixir") iex> List.to_atom(~c"🌢 Elixir")
:"🌢 Elixir" :"🌢 Elixir"
""" """
@doc since: "1.21.0" @spec to_atom(charlist) :: atom
@spec to_unsafe_atom(charlist) :: atom def to_atom(charlist) do
def to_unsafe_atom(charlist) do
:erlang.list_to_atom(charlist) :erlang.list_to_atom(charlist)
end end
@doc """ @doc """
Converts a charlist to an existing atom. Converts a charlist to an existing atom.
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
Elixir supports conversions from charlists which contain any Unicode Elixir supports conversions from charlists which contain any Unicode
code point. Raises an `ArgumentError` if the atom does not exist. code point. Raises an `ArgumentError` if the atom does not exist.
@@ -1096,47 +1023,6 @@ defmodule List do
:erlang.list_to_existing_atom(charlist) :erlang.list_to_existing_atom(charlist)
end end
@doc """
Converts a charlist to one of the `allowed_atoms` or raises.
Raises an `ArgumentError` if the atom either does not exist or is not within
the existing list.
This should be preferred to `to_existing_atom/1` if the list is known upfront,
since there is no risk that the atom has not been loaded.
## Examples
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
:foo
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
"""
@doc since: "1.21.0"
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
atom = :erlang.list_to_existing_atom(charlist)
if atom not in allowed_atoms do
to_existing_atom_unexpected(charlist, allowed_atoms)
end
atom
end
# used just to have a less cryptic stacktrace and consistent error
@doc false
def __to_existing_atom__(charlist, allowed_atoms) do
to_existing_atom_unexpected(charlist, allowed_atoms)
end
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
raise ArgumentError,
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
end
@doc """ @doc """
Returns the float whose text representation is `charlist`. Returns the float whose text representation is `charlist`.
@@ -1148,7 +1034,7 @@ defmodule List do
2.2017764 2.2017764
""" """
@spec to_float(nonempty_charlist) :: float @spec to_float(charlist) :: float
def to_float(charlist) do def to_float(charlist) do
:erlang.list_to_float(charlist) :erlang.list_to_float(charlist)
end end
@@ -1164,7 +1050,7 @@ defmodule List do
123 123
""" """
@spec to_integer(nonempty_charlist) :: integer @spec to_integer(charlist) :: integer
def to_integer(charlist) do def to_integer(charlist) do
:erlang.list_to_integer(charlist) :erlang.list_to_integer(charlist)
end end
@@ -1182,7 +1068,7 @@ defmodule List do
1023 1023
""" """
@spec to_integer(nonempty_charlist, 2..36) :: integer @spec to_integer(charlist, 2..36) :: integer
def to_integer(charlist, base) do def to_integer(charlist, base) do
:erlang.list_to_integer(charlist, base) :erlang.list_to_integer(charlist, base)
end end
@@ -1368,13 +1254,13 @@ defmodule List do
defp myers_difference_with_diff_script(list1, list2, diff_script) do defp myers_difference_with_diff_script(list1, list2, diff_script) do
path = {0, list1, list2, []} path = {0, list1, list2, []}
find_script(0, [path], diff_script) find_script(0, length(list1) + length(list2), [path], diff_script)
end end
defp find_script(envelope, paths, diff_script) do defp find_script(envelope, max, paths, diff_script) do
case each_diagonal(-envelope, envelope, paths, [], diff_script) do case each_diagonal(-envelope, envelope, paths, [], diff_script) do
{:done, edits} -> compact_reverse(edits, []) {:done, edits} -> compact_reverse(edits, [])
{:next, paths} -> find_script(envelope + 1, paths, diff_script) {:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
end end
end end
@@ -1519,16 +1405,15 @@ defmodule List do
# pop_at # pop_at
# The original list is returned when the index is out of bounds defp do_pop_at([], _index, default, acc) do
defp do_pop_at([], _index, default, _acc, original) do {default, :lists.reverse(acc)}
{default, original}
end end
defp do_pop_at([head | tail], 0, _default, acc, _original) do defp do_pop_at([head | tail], 0, _default, acc) do
{head, :lists.reverse(acc, tail)} {head, :lists.reverse(acc, tail)}
end end
defp do_pop_at([head | tail], index, default, acc, original) do defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc], original) do_pop_at(tail, index - 1, default, [head | acc])
end end
end end
+6
View File
@@ -19,6 +19,12 @@ defprotocol List.Chars do
""" """
@spec to_charlist(t) :: charlist @spec to_charlist(t) :: charlist
def to_charlist(term) def to_charlist(term)
@doc false
@deprecated "Use List.Chars.to_charlist/1 instead"
Kernel.def to_char_list(term) do
__MODULE__.to_charlist(term)
end
end end
defimpl List.Chars, for: Atom do defimpl List.Chars, for: Atom do
+97 -189
View File
@@ -166,7 +166,7 @@ defmodule Macro do
of a `__block__` or the right side of `->`. The last expression of the 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 block does not have metadata if it is not followed by an end of line
character (either a newline or `;`). This entry may appear multiple times character (either a newline or `;`). This entry may appear multiple times
in the same metadata if the expression is surrounded by parens in the same metadata if the expression is surround by parens
* `:format` - set to `:keyword` when an atom is defined as a keyword. * `:format` - set to `:keyword` when an atom is defined as a keyword.
It may also be set to `:atom` to distinguish `nil`, `false`, and `true` It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
@@ -235,7 +235,7 @@ defmodule Macro do
`div/2` function, so that the AST for that function will become `{:div, [], `div/2` function, so that the AST for that function will become `{:div, [],
[100, 5]}` (`div(100, 5)`). [100, 5]}` (`div(100, 5)`).
""" """
@spec unpipe(t()) :: [{t(), non_neg_integer}] @spec unpipe(t()) :: [t()]
def unpipe(expr) do def unpipe(expr) do
:lists.reverse(unpipe(expr, [])) :lists.reverse(unpipe(expr, []))
end end
@@ -508,17 +508,19 @@ defmodule Macro do
Generates AST nodes for a given number of required argument Generates AST nodes for a given number of required argument
variables using `Macro.unique_var/2`. variables using `Macro.unique_var/2`.
The second argument is generally the macro caller's module.
## Examples ## Examples
[var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module) iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
iex> {:arg1, [counter: c1], __MODULE__} = var1
iex> {:arg2, [counter: c2], __MODULE__} = var2
iex> is_integer(c1) and is_integer(c2)
true
""" """
@doc since: "1.11.3" @doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: [] @spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) :: @spec generate_unique_arguments(pos_integer, context) ::
[{atom, metadata(), context}, ...] [{atom, [counter: integer], context}, ...]
when context: atom when context: atom
def generate_unique_arguments(amount, context), def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2) do: generate_arguments(amount, context, &unique_var/2)
@@ -527,7 +529,7 @@ defmodule Macro do
defp generate_arguments(amount, context, fun) defp generate_arguments(amount, context, fun)
when is_integer(amount) and amount > 0 and is_atom(context) do when is_integer(amount) and amount > 0 and is_atom(context) do
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context) for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
end end
@doc """ @doc """
@@ -568,15 +570,15 @@ defmodule Macro do
generate another variable, with its own unique counter. generate another variable, with its own unique counter.
See `var/2` for an alternative. See `var/2` for an alternative.
The second argument is generally the macro caller's module.
## Examples ## Examples
var = Macro.unique_var(:foo, __CALLER__.module) iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
iex> is_integer(c)
true
""" """
@doc since: "1.11.3" @doc since: "1.11.3"
@spec unique_var(var, context) :: {var, metadata(), context} @spec unique_var(var, context) :: {var, [counter: integer], context}
when var: atom, context: atom when var: atom, context: atom
def unique_var(var, context) when is_atom(var) and is_atom(context) do def unique_var(var, context) when is_atom(var) and is_atom(context) do
{var, [counter: :elixir_module.next_counter(context)], context} {var, [counter: :elixir_module.next_counter(context)], context}
@@ -690,39 +692,7 @@ defmodule Macro do
""" """
@spec prewalk(t, (t -> t)) :: t @spec prewalk(t, (t -> t)) :: t
def prewalk(ast, fun) when is_function(fun, 1) do def prewalk(ast, fun) when is_function(fun, 1) do
do_prewalk(fun.(ast), fun) elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
end
# Mirrors do_traverse/4 with an always-pre fun and no accumulator,
# avoiding the wrapper closures and tuple threading of traverse/4.
# Each clause dispatches on the already-transformed node, so fun's
# rewrites are descended into, exactly as in traverse/4.
defp do_prewalk({form, meta, args}, fun) when is_atom(form) do
{form, meta, do_prewalk_args(args, fun)}
end
defp do_prewalk({form, meta, args}, fun) do
form = do_prewalk(fun.(form), fun)
{form, meta, do_prewalk_args(args, fun)}
end
defp do_prewalk({left, right}, fun) do
left = do_prewalk(fun.(left), fun)
{left, do_prewalk(fun.(right), fun)}
end
defp do_prewalk(list, fun) when is_list(list) do
do_prewalk_args(list, fun)
end
defp do_prewalk(x, _fun) do
x
end
defp do_prewalk_args(args, _fun) when is_atom(args), do: args
defp do_prewalk_args(args, fun) when is_list(args) do
:lists.map(fn x -> do_prewalk(fun.(x), fun) end, args)
end end
@doc """ @doc """
@@ -760,41 +730,11 @@ defmodule Macro do
""" """
@spec postwalk(t, (t -> t)) :: t @spec postwalk(t, (t -> t)) :: t
def postwalk(ast, fun) when is_function(fun, 1) do def postwalk(ast, fun) when is_function(fun, 1) do
do_postwalk(ast, fun) elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
end
# Mirrors do_traverse/4 with an always-post fun and no accumulator,
# avoiding the wrapper closures and tuple threading of traverse/4
defp do_postwalk({form, meta, args}, fun) when is_atom(form) do
fun.({form, meta, do_postwalk_args(args, fun)})
end
defp do_postwalk({form, meta, args}, fun) do
form = do_postwalk(form, fun)
fun.({form, meta, do_postwalk_args(args, fun)})
end
defp do_postwalk({left, right}, fun) do
left = do_postwalk(left, fun)
fun.({left, do_postwalk(right, fun)})
end
defp do_postwalk(list, fun) when is_list(list) do
fun.(do_postwalk_args(list, fun))
end
defp do_postwalk(x, fun) do
fun.(x)
end
defp do_postwalk_args(args, _fun) when is_atom(args), do: args
defp do_postwalk_args(args, fun) when is_list(args) do
:lists.map(fn x -> do_postwalk(x, fun) end, args)
end end
@doc """ @doc """
This function behaves like `prewalk/3`, but performs a depth-first, This functions behaves like `prewalk/3`, but performs a depth-first,
post-order traversal of quoted expressions using an accumulator. post-order traversal of quoted expressions using an accumulator.
""" """
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any} @spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
@@ -1002,7 +942,7 @@ defmodule Macro do
This is useful when a struct needs to be expanded at This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may compilation time and the struct being expanded may or may
not have been compiled (including structs defined not have been compiled (including structs in the defined
under the module being compiled). For compiled modules, under the module being compiled). For compiled modules,
it will invoke `module.__info__(:struct)`. it will invoke `module.__info__(:struct)`.
@@ -1033,15 +973,9 @@ defmodule Macro do
} }
] ]
def struct_info!(module, env) when is_atom(module) do def struct_info!(module, env) when is_atom(module) do
meta = [line: env.line] case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do {:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
{:ok, info} ->
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
info
{:error, desc} ->
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end end
end end
@@ -1096,7 +1030,7 @@ defmodule Macro do
defp find_invalid(other), do: {:error, other} defp find_invalid(other), do: {:error, other}
@doc """ @doc """
Returns an enumerable that traverses the `ast` in depth-first, Returns an enumerable that traverses the `ast` in depth-first,
pre-order traversal. pre-order traversal.
## Examples ## Examples
@@ -1154,7 +1088,7 @@ defmodule Macro do
end end
@doc """ @doc """
Returns an enumerable that traverses the `ast` in depth-first, Returns an enumerable that traverses the `ast` in depth-first,
post-order traversal. post-order traversal.
## Examples ## Examples
@@ -1257,15 +1191,14 @@ defmodule Macro do
The mapping function receives an integer representing the code point The mapping function receives an integer representing the code point
of the character it wants to unescape. There are also the special atoms of the character it wants to unescape. There are also the special atoms
`:newline`, `:unicode`, and `:hex`, which control newline, unicode, `:newline`, `:unicode`, and `:hex`, which control newline, unicode,
and escaping respectively, and for which the mapping function must return and escaping respectively.
a boolean.
Here is the default mapping function implemented by Elixir: Here is the default mapping function implemented by Elixir:
def unescape_map(:newline), do: true def unescape_map(:newline), do: true
def unescape_map(:unicode), do: true def unescape_map(:unicode), do: true
def unescape_map(:hex), do: true def unescape_map(:hex), do: true
def unescape_map(?0), do: 0 def unescape_map(?0), do: ?0
def unescape_map(?a), do: ?\a def unescape_map(?a), do: ?\a
def unescape_map(?b), do: ?\b def unescape_map(?b), do: ?\b
def unescape_map(?d), do: ?\d def unescape_map(?d), do: ?\d
@@ -1288,11 +1221,7 @@ defmodule Macro do
Macro.unescape_string("example\\n", &unescape_map(&1)) Macro.unescape_string("example\\n", &unescape_map(&1))
""" """
@spec unescape_string( @spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
String.t(),
(non_neg_integer | :newline | :unicode | :hex ->
non_neg_integer | boolean)
) :: String.t()
def unescape_string(string, map) do def unescape_string(string, map) do
:elixir_interpolation.unescape_string(string, map) :elixir_interpolation.unescape_string(string, map)
end end
@@ -1807,9 +1736,9 @@ defmodule Macro do
defp kw_blocks_to_string(kw, fun) do defp kw_blocks_to_string(kw, fun) do
Enum.reduce(unquote(kw_keywords), " ", fn x, acc -> Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
case Keyword.fetch(kw, x) do case Keyword.has_key?(kw, x) do
{:ok, value} -> acc <> kw_block_to_string(x, value, fun) true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
:error -> acc false -> acc
end end
end) <> "end" end) <> "end"
end end
@@ -1923,7 +1852,6 @@ defmodule Macro do
definition compile-time, and you can use `Macro.expand/2`. definition compile-time, and you can use `Macro.expand/2`.
""" """
@doc since: "1.16.0" @doc since: "1.16.0"
@spec compile_apply(module(), atom(), [term()], Macro.Env.t()) :: term()
def compile_apply(mod, fun, args, caller) do def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil}) :elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
Kernel.apply(mod, fun, args) Kernel.apply(mod, fun, args)
@@ -2048,9 +1976,10 @@ defmodule Macro do
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) 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) and env.context != :match do
case Map.fetch(env, field) do if Map.has_key?(env, field) do
{:ok, value} -> {maybe_escape_map(value), true} {maybe_escape_map(Map.get(env, field)), true}
:error -> {original, false} else
{original, false}
end end
end end
@@ -2188,9 +2117,6 @@ defmodule Macro do
def quoted_literal?({:__aliases__, _, args}), def quoted_literal?({:__aliases__, _, args}),
do: quoted_literal?(args) do: quoted_literal?(args)
def quoted_literal?({:__block__, _, [wrapped]}),
do: quoted_literal?(wrapped)
def quoted_literal?({:%, _, [left, right]}), def quoted_literal?({:%, _, [left, right]}),
do: quoted_literal?(left) and quoted_literal?(right) do: quoted_literal?(left) and quoted_literal?(right)
@@ -2266,7 +2192,7 @@ defmodule Macro do
Please check `expand_literals/2` for use cases and pitfalls. Please check `expand_literals/2` for use cases and pitfalls.
""" """
@doc since: "1.14.1" @doc since: "1.14.1"
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term() @spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
def expand_literals(ast, acc, fun) def expand_literals(ast, acc, fun)
def expand_literals({:__aliases__, meta, args}, acc, fun) do def expand_literals({:__aliases__, meta, args}, acc, fun) do
@@ -2570,7 +2496,7 @@ defmodule Macro do
### As a remote call ### As a remote call
Inspect an atom as the function name of a remote call. Inspect an atom the function name of a remote call.
iex> Macro.inspect_atom(:remote_call, :foo) iex> Macro.inspect_atom(:remote_call, :foo)
"foo" "foo"
@@ -2691,76 +2617,54 @@ defmodule Macro do
:unquoted_operator :unquoted_operator
true -> true ->
classify_binary(Atom.to_string(atom), atom) charlist = Atom.to_charlist(atom)
end
end
# ASCII identifiers and aliases are recognized on the binary to avoid building a if valid_alias?(charlist) do
# charlist and running the (unicode aware) tokenizer, which dominates the cost of :alias
# classifying keyword list, map and struct keys. else
defp classify_binary(<<char, rest::binary>>, atom) case :elixir_config.identifier_tokenizer().tokenize(charlist) do
when char >= ?a and char <= ?z {kind, _acc, [], _, _, special} ->
when char == ?_ do cond do
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom) kind != :identifier or :lists.member(:at, special) ->
end :not_callable
defp classify_binary("Elixir" <> rest, atom) do # identifier_tokenizer used to return errors for non-nfc, but
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom) # now it nfc-normalizes everything. However, lack of nfc is
end # still a good reason to quote an atom when printing.
:lists.member(:nfkc, special) ->
:other
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom) true ->
:identifier
end
defp classify_with_tokenizer(atom) do _ ->
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do :other
{kind, _acc, [], _, _, special} -> end
cond do
kind != :identifier or :lists.member(:at, special) ->
:not_callable
# identifier_tokenizer used to return errors for non-nfc, but
# now it nfc-normalizes everything. However, lack of nfc is
# still a good reason to quote an atom when printing.
:lists.member(:nfkc, special) ->
:other
true ->
:identifier
end end
_ ->
:other
end end
end end
defp valid_identifier_rest?(<<char, rest::binary>>) defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
when char >= ?a and char <= ?z defp valid_alias?(_other), do: false
when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9
when char == ?_ do
valid_identifier_rest?(rest)
end
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
defp valid_identifier_rest?(<<>>), do: true do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
defp valid_identifier_rest?(_other), do: false
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z, defp valid_alias_piece?([]), do: true
do: valid_alias_piece_rest?(rest)
defp valid_alias_piece?(<<>>), do: true
defp valid_alias_piece?(_other), do: false defp valid_alias_piece?(_other), do: false
# A helper returning the rest of the binary would build a sub binary per piece, defp trim_leading_while_valid_identifier([char | rest])
# so branch back into valid_alias_piece?/1 to keep the match context.
defp valid_alias_piece_rest?(<<char, rest::binary>>)
when char >= ?a and char <= ?z when char >= ?a and char <= ?z
when char >= ?A and char <= ?Z when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9 when char >= ?0 and char <= ?9
when char == ?_ do when char == ?_ do
valid_alias_piece_rest?(rest) trim_leading_while_valid_identifier(rest)
end end
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other) defp trim_leading_while_valid_identifier(other) do
other
end
@doc """ @doc """
Default backend for `Kernel.dbg/2`. Default backend for `Kernel.dbg/2`.
@@ -2791,7 +2695,7 @@ defmodule Macro do
:guard -> :guard ->
raise ArgumentError, raise ArgumentError,
"invalid expression in guard, dbg is not allowed in guards. " <> "invalid expression in guard, dbg is not allowed in guards. " <>
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html" "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
_ -> _ ->
:ok :ok
@@ -2818,29 +2722,34 @@ defmodule Macro do
# Pipelines. # Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
value_var = unique_var(:value, __MODULE__) value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
[start_ast | rest_asts] = for {ast, 0} <- unpipe(pipe_ast), do: ast [start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)}) rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
first_entry = initial_acc =
quote do quote do
unquote(value_var) = unquote(start_ast) unquote(value_var) = unquote(start_ast)
{:multi_value, unquote(escape(start_ast)), unquote(value_var)} unquote(values_acc_var) = [unquote(value_var)]
end end
len = length(piped_rest_asts) values_ast =
for step_ast <- rest_asts, reduce: initial_acc do
ast_acc ->
quote do
unquote(ast_acc)
unquote(value_var) = unquote(step_ast)
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
end
end
pipe_entries = [
Enum.with_index(piped_rest_asts, fn {original_ast, step_ast}, i -> quote do
tag = if i + 1 == len, do: :pipe_end, else: :pipe unquote(values_ast)
quote do {:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
unquote(value_var) = unquote(step_ast) end
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)} ]
end
end)
[first_entry | pipe_entries]
end end
dbg_decomposed_binary_operators = [:&&, :||, :and, :or] dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
@@ -3046,8 +2955,6 @@ defmodule Macro do
@doc false @doc false
def __dbg__(to_debug, header, options) do def __dbg__(to_debug, header, options) do
{print_location?, options} = Keyword.pop(options, :print_location, true) {print_location?, options} = Keyword.pop(options, :print_location, true)
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
ansi_enabled? = options[:syntax_colors] != [] ansi_enabled? = options[:syntax_colors] != []
if print_location? and is_binary(header) do if print_location? and is_binary(header) do
@@ -3055,24 +2962,25 @@ defmodule Macro do
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?)) :ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
end end
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
{formatted, result} = dbg_format_ast_to_debug(to_debug, options) {formatted, result} = dbg_format_ast_to_debug(to_debug, options)
:ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?)) :ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?))
result result
end end
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
formatted = [ result = List.last(values)
[:faint, "|> ", :reset], code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
dbg_format_ast_with_value_no_newline(code_ast, value, options) [{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
] first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
{formatted, value} rest_formatted =
end Enum.map(asts_with_values, fn {code_ast, value} ->
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
end)
defp dbg_format_ast_to_debug({:pipe_end, code_ast, value}, options) do {[first_formatted | rest_formatted], result}
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
{[formatted, ?\n], value}
end end
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
+15 -16
View File
@@ -203,23 +203,23 @@ defmodule Macro.Env do
Returns a keyword list containing the file and line Returns a keyword list containing the file and line
information as keys. information as keys.
""" """
@spec location(t) :: [file: file, line: line] @spec location(t) :: keyword
def location(env) def location(env)
def location(%{__struct__: Macro.Env, file: file, line: line}) do def location(%{__struct__: Macro.Env, file: file, line: line}) do
[file: file, line: line] [file: file, line: line]
end end
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false @doc false
@deprecated "Use Macro.Env.expand_alias/4 instead"
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom), def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}")) do: Keyword.fetch(aliases, :"Elixir.#{atom}")
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false @doc false
@deprecated "Use Macro.Env.expand_alias/4 instead"
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom) def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
when is_atom(atom), when is_atom(atom),
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}")) do: Keyword.fetch(aliases, :"Elixir.#{atom}")
@doc """ @doc """
Returns the modules from which the given `{name, arity}` was Returns the modules from which the given `{name, arity}` was
@@ -234,7 +234,7 @@ defmodule Macro.Env do
> This function does not emit compiler tracing events, > This function does not emit compiler tracing events,
> which may block the compiler from correctly tracking > which may block the compiler from correctly tracking
> dependencies. Use this function for reflection purposes > dependencies. Use this function for reflection purposes
> but do not use it to expand imports into qualified > but to do not use it to expand imports into qualified
> calls. Instead, use `expand_import/5`. > calls. Instead, use `expand_import/5`.
## Examples ## Examples
@@ -300,7 +300,7 @@ defmodule Macro.Env do
iex> Macro.Env.required?(__ENV__, Integer) iex> Macro.Env.required?(__ENV__, Integer)
false false
iex> require Integer, warn: false iex> require Integer
iex> Macro.Env.required?(__ENV__, Integer) iex> Macro.Env.required?(__ENV__, Integer)
true true
@@ -345,7 +345,7 @@ defmodule Macro.Env do
## Additional options ## Additional options
It accepts the same options as `Kernel.SpecialForms.require/2` plus: It accepts the same options as `Kernel.SpecialForm.require/2` plus:
* #{trace_option} * #{trace_option}
@@ -367,8 +367,7 @@ defmodule Macro.Env do
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: @spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
{:ok, t} | {:error, String.t()}
def define_require(env, meta, module, opts \\ []) def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true) {trace, opts} = Keyword.pop(opts, :trace, true)
@@ -386,7 +385,7 @@ defmodule Macro.Env do
## Additional options ## Additional options
It accepts the same options as `Kernel.SpecialForms.import/2` plus: It accepts the same options as `Kernel.SpecialForm.import/2` plus:
* `:emit_warnings` - emit warnings found when defining imports * `:emit_warnings` - emit warnings found when defining imports
@@ -394,7 +393,7 @@ defmodule Macro.Env do
* `:info_callback` - a function to use instead of `c:Module.__info__/1`. * `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument. The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key-value pairs. 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`. If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples ## Examples
@@ -406,7 +405,7 @@ defmodule Macro.Env do
iex> Macro.Env.lookup_import(env, {:flatten, 1}) iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, List}] [{:function, List}]
It accepts the same options as `Kernel.SpecialForms.import/2`: It accepts the same options as `Kernel.SpecialForm.import/2`:
iex> env = __ENV__ iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:is_odd, 1}) iex> Macro.Env.lookup_import(env, {:is_odd, 1})
@@ -448,7 +447,7 @@ defmodule Macro.Env do
## Additional options ## Additional options
It accepts the same options as `Kernel.SpecialForms.alias/2` plus: It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
* #{trace_option} * #{trace_option}
@@ -658,7 +657,7 @@ defmodule Macro.Env do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env) :elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end end
quoted = expander.(:elixir_dispatch.stop_generated(args), env) quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module) next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end end
@@ -700,7 +699,7 @@ defmodule Macro.Env do
@doc """ @doc """
Returns the environment stacktrace. Returns the environment stacktrace.
""" """
@spec stacktrace(t) :: [{module, atom, arity, keyword}] @spec stacktrace(t) :: list
def stacktrace(%{__struct__: Macro.Env} = env) do def stacktrace(%{__struct__: Macro.Env} = env) do
cond do cond do
is_nil(env.module) -> is_nil(env.module) ->
+55 -106
View File
@@ -201,7 +201,7 @@ defmodule Map do
%{} %{}
""" """
@spec new :: %{} @spec new :: map
def new, do: %{} def new, do: %{}
@doc """ @doc """
@@ -287,13 +287,8 @@ defmodule Map do
@doc """ @doc """
Fetches the value for a specific `key` in the given `map`. Fetches the value for a specific `key` in the given `map`.
If `map` contains the given `key` then its value is returned If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
in the shape of `{:ok, value}`. If `map` doesn't contain `key`, If `map` doesn't contain `key`, `:error` is returned.
`:error` is returned.
If the type system can verify `:error` is always returned
(which means key is never available in the map), it will emit
an error.
Inlined by the compiler. Inlined by the compiler.
@@ -301,7 +296,7 @@ defmodule Map do
iex> Map.fetch(%{a: 1}, :a) iex> Map.fetch(%{a: 1}, :a)
{:ok, 1} {:ok, 1}
iex> Map.fetch(%{"foo" => "bar"}, "unknown") iex> Map.fetch(%{a: 1}, :b)
:error :error
""" """
@@ -312,11 +307,8 @@ defmodule Map do
Fetches the value for a specific `key` in the given `map`, erroring out if Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`. `map` doesn't contain `key`.
The exclamation mark (`!`) implies this function can raise a `KeyError` If `map` contains `key`, the corresponding value is returned. If
exception at runtime if `map` doesn't contain `key`. If the type system `map` doesn't contain `key`, a `KeyError` exception is raised.
can verify this function will always raise (which means the key is never
available), then it will emit a warning at compile-time. See the "Type
checking" section below.
Inlined by the compiler. Inlined by the compiler.
@@ -325,54 +317,11 @@ defmodule Map do
iex> Map.fetch!(%{a: 1}, :a) iex> Map.fetch!(%{a: 1}, :a)
1 1
When the key is missing, an exception is raised:
Map.fetch!(%{a: 1}, :b)
** (KeyError) key :b not found in: %{a: 1}
## Type checking
The compiler will emit a warning if it can verify that
none of the keys given are available in the map.
When the key is an atom, because only single key is given,
a warning will be emitted in case the type system proves
the key is not present.
However, this behaviour matters when the type of the key
represents multiple values. For example:
key = returns_foo_or_bar() #=> :foo or :bar
Map.fetch!(%{foo: 123}, key)
Although the key can be `:foo` or `:bar`, there is no
warning emitted, as `:foo` will succeed. This is by design:
the exclamation mark in Elixir denotes precisely that a
runtime exception may be raised.
In case you are looking up multiple keys and you don't know
if they may be present, you can use `Map.fetch/2` instead
and deal with the error case accordingly:
case Map.fetch(%{foo: 123}, key) do
{:ok, value} -> ...
:error -> ...
end
Both `Map.fetch!/2` and `Map.fetch/2` will emit a warning if
it proves that both `:foo` or `:bar` are absent in the map.
Alternatively, if you want to statically prove that all of keys
are in the map, you can match on the possible values and access
them directly:
case returns_foo_or_bar() do
:foo -> map.foo
:bar -> map.bar
end
""" """
@spec fetch!(map, key) :: value @spec fetch!(map, key) :: value
def fetch!(map, key), do: :maps.get(key, map) def fetch!(map, key) do
:maps.get(key, map)
end
@doc """ @doc """
Puts the given `value` under `key` unless the entry `key` Puts the given `value` under `key` unless the entry `key`
@@ -408,8 +357,8 @@ defmodule Map do
iex> Map.replace(%{a: 1, b: 2}, :a, 3) iex> Map.replace(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2} %{a: 3, b: 2}
iex> Map.replace(%{"a" => 1}, "b", 2) iex> Map.replace(%{a: 1}, :b, 2)
%{"a" => 1} %{a: 1}
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@@ -430,11 +379,7 @@ defmodule Map do
@doc """ @doc """
Puts a value under `key` only if the `key` already exists in `map`. Puts a value under `key` only if the `key` already exists in `map`.
The exclamation mark (`!`) implies this function can raise a `KeyError` If `key` is not present in `map`, a `KeyError` exception is raised.
exception at runtime if `map` doesn't contain `key`. If the type system
can verify this function will always raise (which means the key is never
available), then it will emit a warning at compile-time. See the "Type
checking" section in `Map.fetch!/2` for more information.
Inlined by the compiler. Inlined by the compiler.
@@ -443,8 +388,8 @@ defmodule Map do
iex> Map.replace!(%{a: 1, b: 2}, :a, 3) iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2} %{a: 3, b: 2}
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar") iex> Map.replace!(%{a: 1}, :b, 2)
** (KeyError) key "unknown" not found in: ** (KeyError) key :b not found in:
... ...
""" """
@@ -467,8 +412,8 @@ defmodule Map do
iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end) iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end)
%{a: 4, b: 2} %{a: 4, b: 2}
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end) iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
%{"a" => 1, "b" => 2} %{a: 1, b: 2}
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@@ -547,8 +492,6 @@ defmodule Map do
:erlang.error({:badmap, non_map}) :erlang.error({:badmap, non_map})
end end
defp take([], _map, []), do: %{}
defp take([], _map, acc) do defp take([], _map, acc) do
:maps.from_list(acc) :maps.from_list(acc)
end end
@@ -573,13 +516,15 @@ defmodule Map do
## Examples ## Examples
iex> Map.get(%{"a" => 1}, "a") iex> Map.get(%{}, :a)
1
iex> Map.get(%{"a" => 1}, "b")
nil nil
iex> Map.get(%{"a" => 1}, "b", 3) iex> Map.get(%{a: 1}, :a)
1
iex> Map.get(%{a: 1}, :b)
nil
iex> Map.get(%{a: 1}, :b, 3)
3 3
iex> Map.get(%{"a" => nil}, "a", 1) iex> Map.get(%{a: nil}, :a, 1)
nil nil
""" """
@@ -608,11 +553,15 @@ defmodule Map do
## Examples ## Examples
iex> Map.get_lazy(%{a: 1}, :a, fn -> :expensive_value end) iex> map = %{a: 1}
iex> fun = fn ->
...> # some expensive operation here
...> 13
...> end
iex> Map.get_lazy(map, :a, fun)
1 1
iex> Map.get_lazy(map, :b, fun)
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end) 13
:expensive_value
""" """
@spec get_lazy(map, key, (-> value)) :: value @spec get_lazy(map, key, (-> value)) :: value
@@ -751,10 +700,10 @@ defmodule Map do
iex> Map.pop(%{a: 1}, :a) iex> Map.pop(%{a: 1}, :a)
{1, %{}} {1, %{}}
iex> Map.pop(%{"a" => 1}, "b") iex> Map.pop(%{a: 1}, :b)
{nil, %{"a" => 1}} {nil, %{a: 1}}
iex> Map.pop(%{"a" => 1}, "b", 3) iex> Map.pop(%{a: 1}, :b, 3)
{3, %{"a" => 1}} {3, %{a: 1}}
""" """
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value @spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
@@ -777,8 +726,8 @@ defmodule Map do
{1, %{}} {1, %{}}
iex> Map.pop!(%{a: 1, b: 2}, :a) iex> Map.pop!(%{a: 1, b: 2}, :a)
{1, %{b: 2}} {1, %{b: 2}}
iex> Map.pop!(%{"a" => 1}, "b") iex> Map.pop!(%{a: 1}, :b)
** (KeyError) key "b" not found in: ** (KeyError) key :b not found in:
... ...
""" """
@@ -804,11 +753,15 @@ defmodule Map do
## Examples ## Examples
iex> Map.pop_lazy(%{a: 1}, :a, fn -> :expensive_value end) iex> map = %{a: 1}
iex> fun = fn ->
...> # some expensive operation here
...> 13
...> end
iex> Map.pop_lazy(map, :a, fun)
{1, %{}} {1, %{}}
iex> Map.pop_lazy(map, :b, fun)
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end) {13, %{a: 1}}
{:expensive_value, %{"a" => 1}}
""" """
@spec pop_lazy(map, key, (-> value)) :: {value, map} @spec pop_lazy(map, key, (-> value)) :: {value, map}
@@ -960,8 +913,8 @@ defmodule Map do
iex> Map.update!(%{a: 1}, :a, &(&1 * 2)) iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
%{a: 2} %{a: 2}
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2)) iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
** (KeyError) key "b" not found in: ** (KeyError) key :b not found in:
... ...
""" """
@@ -1080,7 +1033,6 @@ defmodule Map do
#=> %{name: "john"} #=> %{name: "john"}
""" """
# TODO: implement this using row polymorphism
@spec from_struct(atom | struct) :: map @spec from_struct(atom | struct) :: map
def from_struct(struct) when is_atom(struct) do def from_struct(struct) when is_atom(struct) do
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead") IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
@@ -1109,7 +1061,7 @@ defmodule Map do
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2}) iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
false false
Comparison between keys and values is done with `===/2`, Comparison between keys and values is done with `===/3`,
which means integers are not equivalent to floats: which means integers are not equivalent to floats:
iex> Map.equal?(%{a: 1.0}, %{a: 1}) iex> Map.equal?(%{a: 1.0}, %{a: 1})
@@ -1117,7 +1069,11 @@ defmodule Map do
""" """
@spec equal?(map, map) :: boolean @spec equal?(map, map) :: boolean
def equal?(map1, map2)
def equal?(%{} = map1, %{} = map2), do: map1 === map2 def equal?(%{} = map1, %{} = map2), do: map1 === map2
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
@doc false @doc false
@deprecated "Use Kernel.map_size/1 instead" @deprecated "Use Kernel.map_size/1 instead"
@@ -1138,9 +1094,9 @@ defmodule Map do
> #### Performance considerations {: .tip} > #### Performance considerations {: .tip}
> >
> If you find yourself doing multiple calls to `Map.filter/2` > If you find yourself doing multiple calls to `Map.filter/2`
> and/or `Map.reject/2` in a pipeline, it is likely more efficient > and `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to > to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`. > a map at the end using `Map.new/1`.
## Examples ## Examples
@@ -1172,13 +1128,6 @@ defmodule Map do
See also `filter/2`. See also `filter/2`.
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/2`
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`.
## Examples ## Examples
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end) iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
+18 -27
View File
@@ -51,24 +51,11 @@ defmodule MapSet do
that they share many properties, including logarithmic time complexity. Erlang that they share many properties, including logarithmic time complexity. Erlang
`:sets` (version 2) are implemented on top of maps, so see the documentation `:sets` (version 2) are implemented on top of maps, so see the documentation
for `Map` for more information on its execution time complexity. for `Map` for more information on its execution time complexity.
> #### Dialyzer opaqueness warnings {: .warning}
>
> `MapSet` internally relies on the `:sets` module which uses
> opaque types. This might cause Dialyzer to report opaqueness violations.
> These can be silenced by setting the following module attribute:
>
> @dialyzer :no_opaque
""" """
@type value :: term @type value :: term
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined, @opaque internal(value) :: :sets.set(value)
# either via module attributes or by the compiler.
# Defaulting to a broad `term()` type to prevent opaqueness violations.
@typep internal(_value) :: term()
@type t(value) :: %__MODULE__{map: internal(value)} @type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term) @type t :: t(term)
@@ -180,14 +167,18 @@ defmodule MapSet do
@doc since: "1.14.0" @doc since: "1.14.0"
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value @spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
map = {small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
if :sets.is_disjoint(set1, set2) do
:sets.union(set1, set2)
else
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
end
%{map_set1 | map: map} disjointer_fun = fn elem, {small, acc} ->
if :sets.is_element(elem, small) do
{:sets.del_element(elem, small), acc}
else
{small, [elem | acc]}
end
end
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
end end
@doc """ @doc """
@@ -210,7 +201,7 @@ defmodule MapSet do
Checks if two sets are equal. Checks if two sets are equal.
The comparison between elements is done using `===/2`, The comparison between elements is done using `===/2`,
which means a set with `1` is not equivalent to a set with which a set with `1` is not equivalent to a set with
`1.0`. `1.0`.
## Examples ## Examples
@@ -349,7 +340,7 @@ defmodule MapSet do
> If you find yourself doing multiple calls to `MapSet.filter/2` > If you find yourself doing multiple calls to `MapSet.filter/2`
> and `MapSet.reject/2` in a pipeline, it is likely more efficient > and `MapSet.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to > to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a set at the end using `MapSet.new/1`. > a map at the end using `MapSet.new/1`.
## Examples ## Examples
@@ -405,15 +396,15 @@ defmodule MapSet do
iex> while_false iex> while_false
MapSet.new([1, 3]) MapSet.new([1, 3])
iex> {while_true, while_false} = MapSet.split_with(MapSet.new([10, 20, 60, 70]), fn v -> v > 50 end) iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end)
iex> while_true iex> while_true
MapSet.new([60, 70]) MapSet.new([])
iex> while_false iex> while_false
MapSet.new([10, 20]) MapSet.new([])
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec split_with(t(a), (a -> as_boolean(term))) :: {t(a), t(a)} when a: value @spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end) {while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
{%MapSet{map: while_true}, %MapSet{map: while_false}} {%MapSet{map: while_true}, %MapSet{map: while_false}}
+46 -58
View File
@@ -190,7 +190,7 @@ defmodule Module do
* a string (often a heredoc) * a string (often a heredoc)
* `false`, which will make the entity invisible to documentation-extraction * `false`, which will make the entity invisible to documentation-extraction
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
* a keyword list, since Elixir 1.7.0 * a keyword list, since Elixir 1.7.0
For example: For example:
@@ -216,7 +216,7 @@ defmodule Module do
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc` As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
also accept a keyword list that serves as a way to provide arbitrary metadata also accept a keyword list that serves as a way to provide arbitrary metadata
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
`IEx` may use this information to display annotations. A common use `IEx` may use this information to display annotations. A common use
case is the `:since` key, which may be used to annotate in which version the case is the `:since` key, which may be used to annotate in which version the
function was introduced. function was introduced.
@@ -263,7 +263,7 @@ defmodule Module do
Tools may use this information to ensure the module is recompiled Tools may use this information to ensure the module is recompiled
in case any of the external resources change, see for example: in case any of the external resources change, see for example:
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html). [`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
The specified file path provided is interpreted as relative to The specified file path provided is interpreted as relative to
the folder containing the project's `mix.exs`, which is the the folder containing the project's `mix.exs`, which is the
@@ -321,7 +321,7 @@ defmodule Module do
Accepts a string (often a heredoc) or `false` where `@moduledoc false` Accepts a string (often a heredoc) or `false` where `@moduledoc false`
will make the module invisible to documentation extraction tools like will make the module invisible to documentation extraction tools like
[`ExDoc`](https://ex-doc.hexdocs.pm/). [`ExDoc`](https://hexdocs.pm/ex_doc/).
Similarly to `@doc` also accepts a keyword list to provide metadata Similarly to `@doc` also accepts a keyword list to provide metadata
about the module. For more details, see the documentation of `@doc` about the module. For more details, see the documentation of `@doc`
@@ -366,8 +366,7 @@ defmodule Module do
Unlike other hooks, `@on_definition` will only invoke functions and Unlike other hooks, `@on_definition` will only invoke functions and
never macros. This is to avoid `@on_definition` callbacks from never macros. This is to avoid `@on_definition` callbacks from
redefining functions that have just been defined in favor of more redefining functions that have just been defined in favor of more
explicit approaches. They are also invoked in the reverse order of explicit approaches.
registration.
When just a module is provided, the function is assumed to be When just a module is provided, the function is assumed to be
`__on_definition__/6`. `__on_definition__/6`.
@@ -657,8 +656,7 @@ defmodule Module do
@spec module_info(:attributes) :: keyword() @spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword() @spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary() @spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: [function_info] @spec module_info(:nifs) :: module()
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:exports) :: [function_info] @spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()} when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info] @spec module_info(:functions) :: [function_info]
@@ -681,7 +679,7 @@ defmodule Module do
information. For documentation, see `c:Module.__info__/1`. information. For documentation, see `c:Module.__info__/1`.
''' '''
@type definition :: {function_name :: atom, arity} @type definition :: {atom, arity}
@type def_kind :: :def | :defp | :defmacro | :defmacrop @type def_kind :: :def | :defp | :defmacro | :defmacrop
@type create_opts :: [ @type create_opts :: [
@@ -754,7 +752,7 @@ defmodule Module do
""" """
@doc since: "1.12.0" @doc since: "1.12.0"
@spec reserved_attributes() :: %{optional(atom()) => %{doc: binary()}} @spec reserved_attributes() :: map
def reserved_attributes() do def reserved_attributes() do
%{ %{
after_compile: %{ after_compile: %{
@@ -960,7 +958,7 @@ defmodule Module do
It handles binaries and atoms. It handles binaries and atoms.
> #### Untracked compile-time dependencies {: .warning} > #### Untracked compile-time dependencies {. :warning}
> >
> Use this function with care, as dynamically defining > Use this function with care, as dynamically defining
> module names at compilation time may lead to > module names at compilation time may lead to
@@ -986,7 +984,7 @@ defmodule Module do
It handles binaries and atoms. If one of the aliases It handles binaries and atoms. If one of the aliases
is nil, it is discarded. is nil, it is discarded.
> #### Untracked compile-time dependencies {: .warning} > #### Untracked compile-time dependencies {. :warning}
> >
> Use this function with care, as dynamically defining > Use this function with care, as dynamically defining
> module names at compilation time may lead to > module names at compilation time may lead to
@@ -1017,7 +1015,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`. If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms. It handles binaries and atoms.
> #### Untracked compile-time dependencies {: .warning} > #### Untracked compile-time dependencies {. :warning}
> >
> Use this function with care, as dynamically defining > Use this function with care, as dynamically defining
> module names at compilation time may lead to > module names at compilation time may lead to
@@ -1041,7 +1039,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`. If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms. It handles binaries and atoms.
> #### Untracked compile-time dependencies {: .warning} > #### Untracked compile-time dependencies {. :warning}
> >
> Use this function with care, as dynamically defining > Use this function with care, as dynamically defining
> module names at compilation time may lead to > module names at compilation time may lead to
@@ -1148,7 +1146,7 @@ defmodule Module do
defp simplify_var(var, guess_priority) do defp simplify_var(var, guess_priority) do
case Atom.to_string(var) do case Atom.to_string(var) do
"_" -> {:_, [], guess_priority} "_" -> {:_, [], guess_priority}
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority} "_" <> rest -> {String.to_atom(rest), [], guess_priority}
_ -> {var, [], nil} _ -> {var, [], nil}
end end
end end
@@ -1159,7 +1157,7 @@ defmodule Module do
rescue rescue
ArgumentError -> module ArgumentError -> module
else else
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name))) module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
end end
end end
@@ -1192,7 +1190,7 @@ defmodule Module do
defp expand_key(key, counters) do defp expand_key(key, counters) do
case counters do case counters do
%{^key => count} when is_integer(count) and count >= 1 -> %{^key => count} when is_integer(count) and count >= 1 ->
{{String.to_unsafe_atom("#{key}#{count}"), [], Elixir}, Map.put(counters, key, count - 1)} {{:"#{key}#{count}", [], Elixir}, Map.put(counters, key, count - 1)}
_ -> _ ->
{{key, [], Elixir}, counters} {{key, [], Elixir}, counters}
@@ -1225,8 +1223,7 @@ defmodule Module do
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
# Otherwise, returns a generic guess # Otherwise, returns a generic guess
defp merge_signature({_, meta, _}, _newer, i), defp merge_signature({_, meta, _}, _newer, i), do: {:"arg#{i}", meta, Elixir}
do: {String.to_unsafe_atom("arg#{i}"), meta, Elixir}
@doc """ @doc """
Checks if the module defines the given function or macro. Checks if the module defines the given function or macro.
@@ -1252,18 +1249,17 @@ defmodule Module do
""" """
@spec defines?(module, definition) :: boolean @spec defines?(module, definition) :: boolean
def defines?(module, {function_name, arity} = definition) def defines?(module, {name, arity} = tuple)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
arity <= 255 do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?) {set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
:ets.member(set, {:def, definition}) :ets.member(set, {:def, tuple})
end end
@doc """ @doc """
Checks if the module defines a function or macro of the Checks if the module defines a function or macro of the
given kind. given `kind`.
`def_kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`. `kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
This function can only be used on modules that have not yet been compiled. This function can only be used on modules that have not yet been compiled.
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
@@ -1279,13 +1275,12 @@ defmodule Module do
""" """
@spec defines?(module, definition, def_kind) :: boolean @spec defines?(module, definition, def_kind) :: boolean
def defines?(module, {function_name, arity} = definition, def_kind) def defines?(module, {name, arity} = tuple, def_kind)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
arity <= 255 and
def_kind in [:def, :defp, :defmacro, :defmacrop] do def_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?) {set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
case :ets.lookup(set, {:def, definition}) do case :ets.lookup(set, {:def, tuple}) do
[{_, ^def_kind, _, _, _, _}] -> true [{_, ^def_kind, _, _, _, _}] -> true
_ -> false _ -> false
end end
@@ -1298,9 +1293,7 @@ defmodule Module do
""" """
@doc since: "1.7.0" @doc since: "1.7.0"
@spec defines_type?(module, definition) :: boolean @spec defines_type?(module, definition) :: boolean
def defines_type?(module, {function_name, arity} = definition) def defines_type?(module, definition) when is_atom(module) do
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
Kernel.Typespec.defines_type?(module, definition) Kernel.Typespec.defines_type?(module, definition)
end end
@@ -1313,9 +1306,7 @@ defmodule Module do
""" """
@doc since: "1.7.0" @doc since: "1.7.0"
@spec spec_to_callback(module, definition) :: boolean @spec spec_to_callback(module, definition) :: boolean
def spec_to_callback(module, {function_name, arity} = definition) def spec_to_callback(module, definition) do
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
Kernel.Typespec.spec_to_callback(module, definition) Kernel.Typespec.spec_to_callback(module, definition)
end end
@@ -1348,7 +1339,7 @@ defmodule Module do
@doc """ @doc """
Returns all overridable definitions in `module`. Returns all overridable definitions in `module`.
Note a definition is included even if it was already overridden. Note a definition is included even if it was was already overridden.
You can use `defines?/2` to see if a definition exists or one is pending. You can use `defines?/2` to see if a definition exists or one is pending.
This function can only be used on modules that have not yet been compiled. This function can only be used on modules that have not yet been compiled.
@@ -1367,7 +1358,7 @@ defmodule Module do
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec overridables_in(module) :: [definition] @spec overridables_in(module) :: [atom]
def overridables_in(module) when is_atom(module) do def overridables_in(module) when is_atom(module) do
assert_not_compiled!(__ENV__.function, module, :all) assert_not_compiled!(__ENV__.function, module, :all)
:elixir_overridable.overridables_for(module) :elixir_overridable.overridables_for(module)
@@ -1416,10 +1407,10 @@ defmodule Module do
""" """
@spec definitions_in(module, def_kind) :: [definition] @spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, def_kind) def definitions_in(module, kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in) {set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}]) :ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
end end
@doc """ @doc """
@@ -1450,17 +1441,16 @@ defmodule Module do
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]} [{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil | nil
@doc since: "1.12.0" @doc since: "1.12.0"
def get_definition(module, {function_name, arity} = _definition, options \\ []) def get_definition(module, {name, arity}, options \\ [])
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
arity <= 255 and is_list(options) do
{set, bag} = data_tables_for!(module, __ENV__.function, "") {set, bag} = data_tables_for!(module, __ENV__.function, "")
case :ets.lookup(set, {:def, {function_name, arity}}) do case :ets.lookup(set, {:def, {name, arity}}) do
[{_key, kind, meta, _, _, _}] -> [{_key, kind, meta, _, _, _}] ->
clauses = clauses =
if options[:skip_clauses], if options[:skip_clauses],
do: [], do: [],
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2) else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
{:v1, kind, meta, clauses} {:v1, kind, meta, clauses}
@@ -1477,11 +1467,10 @@ defmodule Module do
""" """
@doc since: "1.12.0" @doc since: "1.12.0"
@spec delete_definition(module, definition) :: boolean() @spec delete_definition(module, definition) :: boolean()
def delete_definition(module, {function_name, arity} = _definition) def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and when is_atom(module) and is_atom(name) and is_integer(arity) do
arity <= 255 do
assert_not_compiled!(__ENV__.function, module, :writeable) assert_not_compiled!(__ENV__.function, module, :writeable)
:elixir_def.take_definition(module, {function_name, arity}) != false :elixir_def.take_definition(module, {name, arity}) != false
end end
@doc """ @doc """
@@ -1497,20 +1486,20 @@ defmodule Module do
given. given.
""" """
@spec make_overridable(module, [definition]) :: :ok @spec make_overridable(module, [definition]) :: :ok
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
assert_not_compiled!(__ENV__.function, module, :writeable) assert_not_compiled!(__ENV__.function, module, :writeable)
func = fn func = fn
{function_name, arity} = definition {function_name, arity} = tuple
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 -> when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
case :elixir_def.take_definition(module, definition) do case :elixir_def.take_definition(module, tuple) do
false -> false ->
raise ArgumentError, raise ArgumentError,
"cannot make function #{function_name}/#{arity} " <> "cannot make function #{function_name}/#{arity} " <>
"overridable because it was not defined" "overridable because it was not defined"
clause -> clause ->
:elixir_overridable.record_overridable(module, definition, clause) :elixir_overridable.record_overridable(module, tuple, clause)
end end
other -> other ->
@@ -1519,7 +1508,7 @@ defmodule Module do
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}" "{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
end end
:lists.foreach(func, definitions) :lists.foreach(func, tuples)
end end
@spec make_overridable(module, module) :: :ok @spec make_overridable(module, module) :: :ok
@@ -1577,10 +1566,9 @@ defmodule Module do
exists or one is pending. exists or one is pending.
""" """
@spec overridable?(module, definition) :: boolean @spec overridable?(module, definition) :: boolean
def overridable?(module, {function_name, arity} = definition) def overridable?(module, {function_name, arity} = tuple)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 do
arity <= 255 do :elixir_overridable.overridable_for(module, tuple) != :not_overridable
:elixir_overridable.overridable_for(module, definition) != :not_overridable
end end
@doc """ @doc """
+4 -24
View File
@@ -181,16 +181,8 @@ defmodule Module.Behaviour do
behaviour not in behaviours -> behaviour not in behaviours ->
{:error, {:behaviour_not_declared, behaviour}} {:error, {:behaviour_not_declared, behaviour}}
not Code.ensure_loaded?(behaviour) ->
# Module does not exist, but we have already warned about it.
{:ok, []}
not behaviour_defined?(callbacks, behaviour) ->
# Module does not define behaviour, but we have already warned about it.
{:ok, []}
true -> true ->
{:error, {:callback_not_defined, behaviour, callbacks}} {:error, {:behaviour_not_defined, behaviour, callbacks}}
end end
end end
@@ -223,14 +215,6 @@ defmodule Module.Behaviour do
end end
end end
# Determines whether there is at least one callback defined for the given behaviour.
# If not, that means that the behaviour has not been defined.
defp behaviour_defined?(callbacks, behaviour) do
callbacks
|> Map.values()
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end)
end
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs) defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
when map_size(callbacks) == 0 do when map_size(callbacks) == 0 do
context context
@@ -406,17 +390,13 @@ defmodule Module.Behaviour do
] ]
end end
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
behaviour_string = inspect(behaviour)
[ [
"got \"@impl ", "got \"@impl ",
behaviour_string, inspect(behaviour),
"\" for ", "\" for ",
format_definition(kind, callback), format_definition(kind, callback),
" but ", " but this behaviour does not specify such callback",
behaviour_string,
" does not specify such callback",
known_callbacks(callbacks) known_callbacks(callbacks)
] ]
end end
+108 -163
View File
@@ -10,7 +10,6 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()} @type cache() :: {pid(), :ets.tid()}
@type warning() :: term() @type warning() :: term()
@type error() :: term()
@type mode() :: :erlang | :elixir | :protocol @type mode() :: :erlang | :elixir | :protocol
@typedoc """ @typedoc """
@@ -20,7 +19,6 @@ defmodule Module.ParallelChecker do
{:max_concurrency, pos_integer()} {:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()} | {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())} | {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {:profile, :time}
| {atom(), term()} | {atom(), term()}
] ]
@@ -68,37 +66,17 @@ defmodule Module.ParallelChecker do
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
# Protocols may have been consolidated. So if we know their beam location, # Protocols may have been consolidated. So if we know their beam location,
# we discard their module map on purpose and start from file. # we discard their module map on purpose and start from file.
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do info =
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?) if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
else List.to_string(beam_location)
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures) else
ref = make_ref() cache_from_module_map(table, module_map, signatures)
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
register_cache_and_checker(checker, mode, module, spawned, ref)
:ok
end
end
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
spawn(fn ->
mon_ref = Process.monitor(pid)
receive do
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
Process.link(pid)
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :checked, ref})
{:DOWN, ^mon_ref, _, _, _} ->
:ok
end end
end)
inner_spawn(pid, checker, table, module, info, log?)
end end
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do defp inner_spawn(pid, checker, table, module, info, log?) do
ref = make_ref() ref = make_ref()
spawned = spawned =
@@ -109,28 +87,48 @@ defmodule Module.ParallelChecker do
{^ref, :cache} -> {^ref, :cache} ->
Process.link(pid) Process.link(pid)
location = {mode, module_tuple} =
case :code.which(module) do cond do
[_ | _] = path -> path is_binary(info) ->
_ -> info location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
end
with {:ok, binary} <- File.read(location),
{:ok,
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:not_found, nil}
end
is_tuple(info) ->
info
end end
with {:ok, binary} <- File.read(location), # We only make the module available now, so they are not visible during inference
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <- :ets.insert(table, {module, mode})
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]), send(checker, {ref, :cached})
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(exck) do receive do
mode = cache_chunk(table, module, contents) {^ref, :check} ->
module_tuple = module_map_to_module_tuple(module_map) # Set the compiler info so we can collect warnings
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?) :erlang.put(:elixir_compiler_info, {pid, self()})
:ets.insert(table, {module, mode})
send(checker, {__MODULE__, :cached, module, spawned, ref}) warnings =
else if module_tuple do
_ -> check_module(module_tuple, {checker, table}, log?)
# Nothing to check, so we notify everyone we are done else
:ets.insert(table, {module, :uncached}) []
send(checker, {__MODULE__, :cached, module, nil, ref}) end
send(pid, {__MODULE__, module, [], []})
send(pid, {__MODULE__, module, warnings})
send(checker, {__MODULE__, :done, module})
end end
{:DOWN, ^mon_ref, _, _, _} -> {:DOWN, ^mon_ref, _, _, _} ->
@@ -138,7 +136,7 @@ defmodule Module.ParallelChecker do
end end
end) end)
register_cache(checker, module, spawned, ref) register(checker, module, spawned, ref)
:ok :ok
end end
@@ -186,47 +184,50 @@ defmodule Module.ParallelChecker do
Returns the updated list of warnings from the verification. Returns the updated list of warnings from the verification.
""" """
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]} @spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
def verify({checker, table}, runtime_files) do def verify({checker, table}, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics) value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value) log? = not match?({_, false}, value)
for {module, file} <- runtime_files do for {module, file} <- runtime_files do
spawn_and_register_cache(self(), checker, table, module, file, log?) inner_spawn(self(), checker, table, module, file, log?)
end end
count = :gen_server.call(checker, :start, :infinity) count = :gen_server.call(checker, :start, :infinity)
{warnings, errors} = collect_results(count, [], []) diagnostics = collect_results(count, [])
case :erlang.get(:elixir_code_diagnostics) do case :erlang.get(:elixir_code_diagnostics) do
:undefined -> :ok :undefined -> :ok
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?}) {tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
end end
{warnings, errors} diagnostics
end end
defp collect_results(0, warnings, errors) do defp collect_results(0, diagnostics) do
{warnings, errors} diagnostics
end end
defp collect_results(count, warnings, errors) do defp collect_results(count, diagnostics) do
receive do receive do
{:diagnostic, %{file: file} = diagnostic, read_snippet} -> {:diagnostic, %{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet) :elixir_errors.print_diagnostic(diagnostic, read_snippet)
diagnostic = %{diagnostic | file: file && Path.absname(file)} diagnostic = %{diagnostic | file: file && Path.absname(file)}
collect_results(count, [diagnostic | diagnostics])
if Map.get(diagnostic, :severity, :warning) == :error do {__MODULE__, _module, new_diagnostics} ->
collect_results(count, warnings, [diagnostic | errors]) collect_results(count - 1, new_diagnostics ++ diagnostics)
else
collect_results(count, [diagnostic | warnings], errors)
end
{__MODULE__, _module, new_warnings, new_errors} ->
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
end end
end end
@doc """
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
end
@doc """ @doc """
Returns the export kind and deprecation reason for the given MFA from Returns the export kind and deprecation reason for the given MFA from
the cache. If the module does not exist return `:badmodule`, the cache. If the module does not exist return `:badmodule`,
@@ -263,7 +264,7 @@ defmodule Module.ParallelChecker do
## Module checking ## Module checking
defp check_module(module_tuple, cache, log?, profile) do defp check_module(module_tuple, cache, log?) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} = {module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
module_tuple module_tuple
@@ -277,23 +278,18 @@ defmodule Module.ParallelChecker do
definitions definitions
) )
type_warnings = diagnostics =
profile(module, profile, fn -> module
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache) |> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
type_warnings
|> Kernel.++(behaviour_warnings) |> Kernel.++(behaviour_warnings)
|> group_diagnostics() |> group_warnings()
|> emit_diagnostics(file, log?) |> emit_warnings(file, log?)
|> Enum.split_with(&(&1.severity == :warning))
Enum.each(after_verify, fn {verify_mod, verify_fun} -> Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module]) apply(verify_mod, verify_fun, [module])
end) end)
{warnings, errors} diagnostics
end end
defp module_map_to_module_tuple(module_map) do defp module_map_to_module_tuple(module_map) do
@@ -344,16 +340,16 @@ defmodule Module.ParallelChecker do
## Warning helpers ## Warning helpers
defp group_diagnostics(triplets) do defp group_warnings(warnings) do
{ungrouped, grouped} = {ungrouped, grouped} =
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} -> Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(term) %{message: _} = diagnostic = module.format_diagnostic(warning)
if Map.get(diagnostic, :group, false) do if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location]) locations = MapSet.new([location])
grouped = grouped =
Map.update(grouped, term, {locations, diagnostic}, fn Map.update(grouped, warning, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic} {locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end) end)
@@ -371,7 +367,7 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped) Enum.sort(ungrouped ++ grouped)
end end
defp emit_diagnostics(warnings, file, log?) do defp emit_warnings(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} -> Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1)) diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics) log? and print_diagnostics(diagnostics)
@@ -533,12 +529,8 @@ defmodule Module.ParallelChecker do
:gen_server.call(server, {:unlock, module, mode}, :infinity) :gen_server.call(server, {:unlock, module, mode}, :infinity)
end end
defp register_cache_and_checker(server, mode, module, pid, ref) do defp register(server, module, pid, ref) do
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref}) :gen_server.cast(server, {:register, module, pid, ref})
end
defp register_cache(server, module, pid, ref) do
:gen_server.cast(server, {:register_cache, module, pid, ref})
end end
## Server callbacks ## Server callbacks
@@ -581,13 +573,11 @@ defmodule Module.ParallelChecker do
state = %{ state = %{
waiting: %{}, waiting: %{},
caches: [], modules: [],
checkers: [],
spawned: %{}, spawned: %{},
schedulers: schedulers, schedulers: schedulers,
threshold: threshold, threshold: threshold,
callback: callback, callback: callback,
profile: Keyword.get(options, :profile),
protocols: [], protocols: [],
table: table table: table
} }
@@ -595,9 +585,20 @@ defmodule Module.ParallelChecker do
:gen_server.enter_loop(__MODULE__, [], state) :gen_server.enter_loop(__MODULE__, [], state)
end end
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
:ets.insert(table, Enum.map(protocols, &{&1, :uncached})) :ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
{:reply, length(caches), run_caches(%{state | protocols: []})}
for {_module, pid, ref} <- modules do
send(pid, {ref, :cache})
end
for {_module, _pid, ref} <- modules do
receive do
{^ref, :cached} -> :ok
end
end
{:reply, length(modules), run_checkers(%{state | protocols: []})}
end end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
@@ -627,21 +628,10 @@ defmodule Module.ParallelChecker do
{:noreply, state} {:noreply, state}
end end
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do def handle_info({__MODULE__, :done, module}, state) do
{_nil, spawned} = Map.pop(state.spawned, ref) # Unfortunately we cannot assume uniqueness because the same module
# may be defined by mistake several times
state = {timer, spawned} = Map.pop(state.spawned, module)
if pid do
%{state | spawned: spawned, checkers: [{module, pid, ref} | state.checkers]}
else
%{state | spawned: spawned}
end
{:noreply, run_caches(state)}
end
def handle_info({__MODULE__, :checked, ref}, state) do
{timer, spawned} = Map.pop(state.spawned, ref)
timer && Process.cancel_timer(timer) timer && Process.cancel_timer(timer)
{:noreply, run_checkers(%{state | spawned: spawned})} {:noreply, run_checkers(%{state | spawned: spawned})}
end end
@@ -650,41 +640,11 @@ defmodule Module.ParallelChecker do
{:stop, :normal, state} {:stop, :normal, state}
end end
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
{:noreply, %{state | caches: [{module, pid, ref} | caches]}} {:noreply, %{state | modules: [{module, pid, ref} | modules]}}
end end
def handle_cast( defp run_checkers(%{modules: []} = state) do
{:register_cache_and_checker, mode, module, pid, ref},
%{caches: caches, checkers: checkers} = state
) do
{:noreply,
%{state | caches: [{module, mode} | caches], checkers: [{module, pid, ref} | checkers]}}
end
defp run_caches(%{caches: [], spawned: spawned} = state) do
if spawned == %{}, do: run_checkers(state), else: state
end
defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
when map_size(spawned) >= schedulers do
state
end
defp run_caches(%{caches: [cache | caches]} = state) do
case cache do
{_module, pid, ref} ->
send(pid, {ref, :cache})
spawned = Map.put(state.spawned, ref, nil)
run_caches(%{state | caches: caches, spawned: spawned})
{module, mode} ->
:ets.insert(state.table, {module, mode})
run_caches(%{state | caches: caches})
end
end
defp run_checkers(%{checkers: []} = state) do
state state
end end
@@ -693,25 +653,10 @@ defmodule Module.ParallelChecker do
state state
end end
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check, state.profile}) send(pid, {ref, :check})
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid}) timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
spawned = Map.put(state.spawned, ref, timer) spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | checkers: checkers, spawned: spawned}) run_checkers(%{state | modules: modules, spawned: spawned})
end
defp profile(module, :time, fun) do
{time, result} = :timer.tc(fun)
IO.puts(
:stderr,
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
)
result
end
defp profile(_module, _profile, fun) do
fun.()
end end
end end
+90 -343
View File
@@ -4,7 +4,7 @@
defmodule Module.Types do defmodule Module.Types do
@moduledoc false @moduledoc false
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern} alias Module.Types.{Descr, Expr, Pattern, Helpers}
# The mode controls what happens on function application when # The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always # there are gradual arguments. Non-gradual arguments always
@@ -24,17 +24,20 @@ defmodule Module.Types do
# #
# * :infer - Same as :dynamic but skips remote calls. # * :infer - Same as :dynamic but skips remote calls.
# #
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided). # The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always # We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on # intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback). # feedback).
@modes [:static, :dynamic, :infer] @modes [:static, :dynamic, :infer, :traversal]
# These functions are not inferred because they are added/managed by the compiler # These functions are not inferred because they are added/managed by the compiler
@no_infer [behaviour_info: 1] @no_infer [behaviour_info: 1]
@doc false @doc false
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
# We don't care about inferring signatures for protocols, # We don't care about inferring signatures for protocols,
# those will be replaced anyway. There is also nothing to # those will be replaced anyway. There is also nothing to
# infer if there is no cache system, we only do traversals. # infer if there is no cache system, we only do traversals.
@@ -72,13 +75,12 @@ defmodule Module.Types do
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler) stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
# In case there are loops, the other we traverse matters, {types, %{local_sigs: reachable_sigs} = context} =
# so we sort the definitions for determinism for {fun_arity, kind, meta, _clauses} = def <- defs,
{types, private, %{local_sigs: reachable_sigs} = context} = kind in [:def, :defmacro],
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs), reduce: {[], context()} do
reduce: {[], [], context()} do {types, context} ->
{types, private, context} when kind in [:def, :defmacro] -> # Optimized version of finder, since we already the definition
# Optimized version of finder, since we already have the definition
finder = fn _ -> finder = fn _ ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl) default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end end
@@ -86,13 +88,10 @@ defmodule Module.Types do
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder) {_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context} {[{fun_arity, inferred} | types], context}
else else
{types, private, context} {types, context}
end end
{types, private, context} ->
{types, [def | private], context}
end end
# Now traverse all used privates to find any other private that have been used by them. # Now traverse all used privates to find any other private that have been used by them.
@@ -106,8 +105,8 @@ defmodule Module.Types do
{unreachable, _context} = {unreachable, _context} =
Enum.reduce(private, {[], context}, fn Enum.reduce(private, {[], context}, fn
{fun_arity, kind, meta, _clauses}, {unreachable, context} -> {fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
warn_unused_def(fun_arity, kind, meta, used_sigs, env) warn_unused_def(info, used_sigs, env)
# Find anything undefined within unused functions # Find anything undefined within unused functions
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder) {_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
@@ -126,7 +125,7 @@ defmodule Module.Types do
end end
defp infer_mode(kind, infer_signatures?) do defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
end end
defp protocol?(attrs) do defp protocol?(attrs) do
@@ -136,8 +135,8 @@ defmodule Module.Types do
defp impl_for(attrs) do defp impl_for(attrs) do
case List.keyfind(attrs, :__impl__, 0) do case List.keyfind(attrs, :__impl__, 0) do
{:__impl__, [protocol: protocol, for: for]} -> {:__impl__, [protocol: protocol, for: for]} ->
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
{for, protocol.__protocol__(:functions)} {for, protocol.behaviour_info(:callbacks)}
else else
nil nil
end end
@@ -155,7 +154,7 @@ defmodule Module.Types do
| List.duplicate(Descr.dynamic(), arity - 1) | List.duplicate(Descr.dynamic(), arity - 1)
] ]
{_fun_arity, kind, meta, clauses} = def {fun_arity, kind, meta, clauses} = def
clauses = clauses =
for {meta, args, guards, body} <- clauses do for {meta, args, guards, body} <- clauses do
@@ -174,30 +173,29 @@ defmodule Module.Types do
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple) :elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end end
defp warn_unused_def(fun_arity, kind, meta, used, env) do defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
default = Keyword.get(meta, :defaults, 0) :ok
end
cond do defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true -> case is_map_key(used, fun_arity) do
:ok true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
default == 0 -> :ok
case is_map_key(used, fun_arity) do end
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
default > 0 -> defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
{name, arity} = fun_arity {name, arity} = tuple
min = arity - default min = arity - default
max = arity max = arity
case min_reachable_default(max, min, :none, name, used) do case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind}) :none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok ^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity}) ^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff}) diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
end
end end
:ok :ok
@@ -232,7 +230,7 @@ defmodule Module.Types do
context = context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context -> Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already have the definition # Optimized version of finder, since we already the definition
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder) {_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context context
@@ -251,21 +249,15 @@ defmodule Module.Types do
context -> context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity) {_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
{used_indexes, unused_indexes} = clauses_indexes =
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index}, for type_index <- pending,
{used_indexes, unused_indexes} -> not skip_unused_clause?(info, type_index),
if type_index in pending and not skip_unused_clause?(info, type_index) do {clause_index, ^type_index} <- mapping,
{used_indexes, [clause_index | unused_indexes]} do: clause_index
else
{[clause_index | used_indexes], unused_indexes}
end
end)
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes Enum.reduce(clauses_indexes, context, fn clause_index, context ->
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index) {meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity} |> with_file_meta(meta) stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context) Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end) end)
end end
@@ -295,11 +287,11 @@ defmodule Module.Types do
local_sigs -> local_sigs ->
case finder.(fun_arity) do case finder.(fun_arity) do
{mode, {fun_arity, kind, _meta, clauses}, expected} -> {mode, {fun_arity, kind, meta, clauses}, expected} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind)) context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} = {inferred, mapping, context} =
local_handler(mode, fun_arity, kind, clauses, expected, stack, context) local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
context = context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping})) update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -312,245 +304,62 @@ defmodule Module.Types do
end end
end end
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
context =
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
stack = with_file_meta(stack, meta)
Module.Types.Traverse.of_expr(body, stack, context)
end)
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
{fun, _arity} = fun_arity {fun, _arity} = fun_arity
stack = fresh_stack(stack, mode, fun_arity) stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
base_info = {:def, kind, fun, expected}
case clauses do {_, _, mapping, clauses_types, clauses_context} =
[{meta, args, [], {:super, _, [_ | _]} = body}] -> Enum.reduce(clauses, {0, 0, [], [], context}, fn
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) {meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
_ ->
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
end
end
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
stack = with_file_meta(stack, meta)
guards = []
previous = Pattern.init_previous()
fresh_context = fresh_context(context)
info = {base_info, args, guards}
try do
{trees, _, _, _, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
# Compute the intersected arrows from the function call
{:super, meta, call_args} = body
{_kind, call_fun} = Keyword.fetch!(meta, :super)
term = Descr.term()
of_fun = &Expr.of_expr/5
{arrows, body_context} =
Apply.local_arrows(call_fun, call_args, term, body, stack, head_context, of_fun)
# For each arrow, compute the default arrow
{_, _, mapping, inferred} =
Enum.reduce(arrows, {0, 0, [], []}, fn
{clause_domain, return_type}, {index, total, mapping, inferred} ->
of_fun = &Expr.of_expr(&1, &2, body, stack, &3)
{_clause_args, clause_context} =
Helpers.zip_map_reduce(call_args, clause_domain, head_context, of_fun)
clause_types = Pattern.of_domain(trees, stack, clause_context)
{type_index, inferred} =
add_inferred(inferred, clause_types, return_type, total - 1, [])
total = if type_index == -1, do: total + 1, else: total
{index + 1, total, [{0, index} | mapping], inferred}
end)
domain =
case inferred do
[_] ->
nil
_ ->
inferred
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.opt_union/2) end)
end
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
{_, clauses_types, clauses_context} =
Enum.reduce(clauses, {Pattern.init_previous(), [], context}, fn
{meta, args, guards, body}, {previous, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
try do try do
{trees, precise?, head_no_previous_args_types, previous, head_context} = {trees, context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context) Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context)
{return_type, context} = {return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, head_context) Expr.of_expr(body, Descr.term(), body, stack, context)
args_types = Pattern.of_domain(trees, stack, context) args_types =
if stack.mode == :traversal do
head_args_types = expected
case inferred do else
[] -> nil Pattern.of_domain(trees, expected, context)
_ -> Pattern.of_domain(trees, stack, head_context)
end end
args_triplet = {args_types, head_args_types, head_no_previous_args_types} {type_index, inferred} =
inferred = [{args_triplet, return_type, precise?} | inferred] add_inferred(inferred, args_types, return_type, total - 1, [])
{previous, inferred, context}
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
end
rescue rescue
e -> e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack) internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end end
end) end)
{clauses_types, mapping, domain} =
clauses_types
|> Enum.reverse()
|> group_clauses()
domain = domain =
case clauses_types do case clauses_types do
[_] -> nil [_] ->
_ -> domain nil
_ ->
clauses_types
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
end end
inferred = {:infer, domain, clauses_types} inferred = {:infer, domain, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(clauses_context, context)} {inferred, mapping, restore_context(clauses_context, context)}
end end
defp group_clauses(clauses) do
{_, all_clauses, filtered_clauses, non_empty?} =
Enum.reduce(clauses, {0, [], [], false}, fn
{_args_triplet, return, precise?} = clause,
{index, all_clauses, filtered_clauses, non_empty?} ->
empty? = Descr.empty?(return)
indexed_clause = {clause, index}
filtered_clauses =
if precise? and empty? do
filtered_clauses
else
[indexed_clause | filtered_clauses]
end
{index + 1, [indexed_clause | all_clauses], filtered_clauses, non_empty? or not empty?}
end)
clauses =
if non_empty? do
Enum.reverse(filtered_clauses)
else
Enum.reverse(all_clauses)
end
[
{{{args, _head_args, _head_no_previous_args}, _return, _precise?}, _index}
| clauses_tail
] = clauses
domain =
Enum.reduce(clauses_tail, args, fn
{{{args, head_args, head_no_previous_args}, _return, _precise?}, _index}, domain ->
compute_domain(args, head_args, head_no_previous_args, domain)
end)
{_, mapping, inferred} =
Enum.reduce(clauses, {0, [], []}, fn
{{{args, _head_args, _head_no_previous_args}, return, _precise?}, index},
{total, mapping, inferred} ->
{type_index, inferred} = add_inferred(inferred, args, return, total - 1, [])
if type_index == -1 do
{total + 1, [{index, total} | mapping], inferred}
else
{total, [{index, type_index} | mapping], inferred}
end
end)
{Enum.reverse(inferred), mapping, domain}
end
defp compute_domain(
[arg | args_types],
[head_arg | head_args_types],
[no_prev_arg | no_prev_args_types],
[d | domain]
) do
[
# This is an optimization that broadens the domain, but it is acceptable
# because the domain is used for reverse arrows and not type checking.
#
# The overall idea is that, if we have a function with three clauses,
# the domain is computed by unioning their inferred types. However, their
# inferred types often have the different of the previous clauses:
#
# opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
#
# Where `rN` represents the refinement in every function body.
#
# What this function does is, if the type of a given arg in a clause
# before and after the body is the same (meaning r3 is term), then
# we replace all of `(c3 - c2 - c1)` by just `c3`, which removes
# many of the differences in the node. However, keep in mind that,
# because `r2` may have refine `c2` in the previous clause, the domain
# may end-up being broader. Take this example:
#
# % %{..., foo: integer()} -> binary()
# def example(%{foo: var}), do: Integer.to_string(var)
#
# % %{...} and not %{..., foo: term()} -> :error
# def example(%{}), do: :error
#
# The actual domain is:
#
# %{..., foo: not_set()} or %{..., foo: integer()}
# #=> %{..., foo: if_set(integer())}
#
# But we will infer:
#
# %{...} or %{..., foo: integer()}
# #=> %{...}
#
# We lose precision but this is exactly what we want: to have simpler types.
# Furthermore, the signature used in type checking is not refined in any way,
# so type checking is still sound.
if arg == head_arg do
Descr.opt_union(Descr.upper_bound(no_prev_arg), d)
else
Descr.opt_union(arg, d)
end
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
]
end
defp compute_domain([], [], [], []), do: []
# We check for term equality of types as an optimization # We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime. # to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc), defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])} do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, index, acc), defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc]) do: add_inferred(tail, args, return, index - 1, [head | acc])
@@ -558,58 +367,6 @@ defmodule Module.Types do
defp add_inferred([], args, return, -1, acc), defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]} do: {-1, [{args, return} | Enum.reverse(acc)]}
# Compact clauses that have the same return and differ in exactly one
# argument by unioning that argument. For example:
#
# (integer(), atom() -> boolean()) and (float(), atom() -> boolean())
#
# becomes:
#
# (number(), atom() -> boolean())
#
# Arity-zero clauses have no argument position to widen.
defp group_clauses_by_return({:infer, domain, [{[_ | _], _} | _] = clauses}) do
clauses =
Enum.reduce(clauses, [], fn {args, return}, acc ->
group_clause_by_return(acc, args, return)
end)
{:infer, domain, clauses}
end
defp group_clauses_by_return(info), do: info
defp group_clause_by_return([{existing_args, return} | tail], args, return) do
case union_args(existing_args, args, [], false) do
nil ->
[{existing_args, return} | group_clause_by_return(tail, args, return)]
new_args ->
[{new_args, return} | tail]
end
end
defp group_clause_by_return([head | tail], args, return) do
[head | group_clause_by_return(tail, args, return)]
end
defp group_clause_by_return([], args, return), do: [{args, return}]
defp union_args([arg | existing], [arg | args], acc, changed?) do
union_args(existing, args, [arg | acc], changed?)
end
# Allow exactly one differing argument. That one position is widened
# with union/2. A second difference means the clauses must stay separate.
defp union_args([existing_arg | existing], [arg | args], acc, false) do
union_args(existing, args, [Descr.opt_union(existing_arg, arg) | acc], true)
end
defp union_args([_ | _], [_ | _], _acc, true), do: nil
# In theory fully equal args are merged on add_inferred
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
defp with_file_meta(stack, meta) do defp with_file_meta(stack, meta) do
case Keyword.fetch(meta, :file) do case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file} {:ok, {meta_file, _}} -> %{stack | file: meta_file}
@@ -672,8 +429,9 @@ defmodule Module.Types do
mode: mode, mode: mode,
# The function for handling local calls # The function for handling local calls
local_handler: handler, local_handler: handler,
# Reverse arrow handling (nil | :cache | :except_none | :include_none) # Control if variable refinement is enabled.
reverse_arrow: nil # It is disabled only on dynamic dispatches.
refine_vars: false
} }
end end
@@ -684,45 +442,34 @@ defmodule Module.Types do
warnings: [], warnings: [],
# All vars and their types # All vars and their types
vars: %{}, vars: %{},
# Stores special metadata used by list heads and domain keys in patterns # Variables and arguments from patterns
subpatterns: %{},
# Variables that are specific to the current environment/conditional
conditional_vars: nil,
# Track metadata specific to patterns and guards
pattern_info: nil, pattern_info: nil,
# If type checking has found an error/failure # If type checking has found an error/failure
failed: false, failed: false,
# Local signatures used by local handler # Local signatures used by local handler
local_sigs: %{}, local_sigs: %{},
# Track which clauses have been used across private local calls # Track which clauses have been used across private local calls
local_used: %{}, local_used: %{}
# Cached reverse arrows
reverse_arrows: %{}
} }
end end
defp fresh_stack(stack, mode, function) when mode in @modes do defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function, reverse_arrow: nil} %{stack | mode: mode, function: function}
end end
defp fresh_context(context) do defp fresh_context(context) do
%{context | vars: %{}, failed: false, reverse_arrows: %{}} %{context | vars: %{}, failed: false}
end end
defp restore_context(later_context, %{ defp restore_context(later_context, %{vars: vars, failed: failed}) do
vars: vars, %{later_context | vars: vars, failed: failed}
failed: failed,
reverse_arrows: reverse_arrows
}) do
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
end end
## Diagnostics ## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{ %{
message: message: "this clause of #{kind} #{fun}/#{arity} is never used"
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
} }
end end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+40 -141
View File
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
@doc """ @doc """
Returns true if the mode cares about warnings. Returns true if the mode cares about warnings.
""" """
defguard is_warning(stack) when stack.mode != :infer defguard is_warning(stack) when stack.mode not in [:traversal, :infer]
@doc """ @doc """
Guard function to check if an AST node is a variable. Guard function to check if an AST node is a variable.
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
## Warnings ## Warnings
@doc """ @doc """
Converts an integer into ordinal. Converts an itneger into ordinal.
""" """
def integer_to_ordinal(i) do def integer_to_ordinal(i) do
case rem(i, 10) do case rem(i, 10) do
@@ -143,13 +143,12 @@ defmodule Module.Types.Helpers do
""" """
def collect_traces(expr, %{vars: vars}) do def collect_traces(expr, %{vars: vars}) do
{_, versions} = {_, versions} =
Macro.prewalk(expr, %{}, fn node, versions -> Macro.prewalk(expr, %{}, fn
with {var_name, meta, var_context} when is_atom(var_name) and is_atom(var_context) <- node, {var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
false <- String.starts_with?(Atom.to_string(var_name), "_") do
version = meta[:version] version = meta[:version]
case vars do case vars do
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} -> %{^version => %{off_traces: off_traces, name: name, context: context}} ->
{:ok, {:ok,
Map.put(versions, version, %{ Map.put(versions, version, %{
type: :variable, type: :variable,
@@ -161,9 +160,9 @@ defmodule Module.Types.Helpers do
_ -> _ ->
{:ok, versions} {:ok, versions}
end end
else
_ -> {node, versions} node, versions ->
end {node, versions}
end) end)
versions versions
@@ -262,8 +261,7 @@ defmodule Module.Types.Helpers do
Converts the given expression to a string, Converts the given expression to a string,
translating inlined Erlang calls back to Elixir. translating inlined Erlang calls back to Elixir.
We also undo some macro expressions done by the Kernel module We also undo some macro expressions done by the Kernel module.
and collapse complex expressions.
## Options ## Options
@@ -329,13 +327,6 @@ defmodule Module.Types.Helpers do
end end
end end
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
call
else
{:in, meta, [expr, args]}
end
{{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} -> {{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} ->
{:to_string, meta, [arg]} {:to_string, meta, [arg]}
@@ -361,134 +352,42 @@ defmodule Module.Types.Helpers do
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} {:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
{:case, meta, [expr, [do: clauses]]} -> {:case, meta, [expr, [do: clauses]]} = case ->
case meta[:type_check] do if meta[:type_check] == :expr do
{:case, op} -> case clauses do
case clauses do [
[ {:->, _,
{:->, _, [
[ [
[ {:when, _,
{:when, _, [
[ {var, _, Kernel},
{var, _, Kernel}, {{:., _, [:erlang, :orelse]}, _,
{{:., _, [:erlang, :orelse]}, _, [
[ {{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]}, {{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]} ]}
]} ]}
]} ],
], else_block
true ]},
]}, {:->, _, [[{:_, _, Kernel}], do_block]}
{:->, _, [[{:_, _, Kernel}], false]} ] ->
] {:if, meta, [expr, [do: do_block, else: else_block]]}
when op == :! ->
{:!, meta, [expr]}
[ [
{:->, _, {:->, _, [[false], else_block]},
[ {:->, _, [[true], do_block]}
[ ] ->
{:when, _, {:if, meta, [expr, [do: do_block, else: else_block]]}
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
]
when op == :|| ->
{:||, meta, [expr, right_side]}
[ _ ->
{:->, _, case
[ end
[ else
{:when, _, case
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
{var, _, Kernel}
]},
{:->, _, [[{:_, _, Kernel}], right_side]}
]
when op == :&& ->
{:&&, meta, [expr, right_side]}
[
{:->, _,
[
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
else_block
]},
{:->, _, [[{:_, _, Kernel}], do_block]}
]
when op == :if ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _, [[false], else_block]},
{:->, _, [[true], do_block]}
]
when op == :if ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _, [[false], false]},
{:->, _, [[true], right]}
| _
]
when op == :and ->
{:and, meta, [expr, right]}
[
{:->, _, [[false], right]},
{:->, _, [[true], true]}
| _
]
when op == :or ->
{:or, meta, [expr, right]}
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end end
{:try, meta, [[do: _] ++ _]} ->
{:try, meta, [[do: {:..., [], []}]]}
{:cond, meta, [[do: _]]} ->
{:cond, meta, [[do: {:..., [], []}]]}
{:receive, meta, [[do: _] ++ _]} ->
{:receive, meta, [[do: {:..., [], []}]]}
{var, meta, context} = expr when is_atom(var) and is_atom(context) -> {var, meta, context} = expr when is_atom(var) and is_atom(context) ->
if is_integer(meta[:capture]) do if is_integer(meta[:capture]) do
{:&, meta, [meta[:capture]]} {:&, meta, [meta[:capture]]}
+220 -477
View File
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team # SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Module.Types.Of do defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern. # Typing functionality shared between Expr and Pattern.
@@ -11,11 +10,11 @@ defmodule Module.Types.Of do
@prefix quote(do: ...) @prefix quote(do: ...)
@suffix quote(do: ...) @suffix quote(do: ...)
@integer_or_float opt_union(integer(), float()) @integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer() @integer integer()
@float float() @float float()
@binary binary() @binary binary()
@bitstring bitstring()
## Variables ## Variables
@@ -30,53 +29,19 @@ defmodule Module.Types.Of do
@doc """ @doc """
Marks a variable with error. Marks a variable with error.
This purposely deletes all traces of the variable,
as it is often invoked when the cause for error is elsewhere.
""" """
def error_var({_, meta, _}, context) do def error_var(var, context) do
error_var(Keyword.fetch!(meta, :version), context)
end
def error_var(version, context) do
update_in(context.vars[version], fn
%{errored: true} = data -> data
data -> Map.put(%{data | type: error_type(), off_traces: []}, :errored, true)
end)
end
@doc """
Declares a variable.
"""
def declare_var(var, type \\ term(), context) do
{var_name, meta, var_context} = var {var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version) version = Keyword.fetch!(meta, :version)
case context.vars do data = %{
%{^version => _} -> type: error_type(),
context name: var_name,
context: var_context,
off_traces: []
}
vars -> put_in(context.vars[version], data)
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: [],
paths: [],
deps: %{}
}
%{context | vars: Map.put(vars, version, data)}
end
end
@doc """
Tracks metadata about variables dependencies and paths.
"""
def track_var(version, new_deps, new_paths, context) do
update_in(context.vars[version], fn %{paths: paths, deps: deps} = data ->
%{data | paths: new_paths ++ paths, deps: Enum.reduce(new_deps, deps, &Map.put(&2, &1, []))}
end)
end end
@doc """ @doc """
@@ -86,70 +51,26 @@ defmodule Module.Types.Of do
or if we are doing a guard analysis or occurrence typing. or if we are doing a guard analysis or occurrence typing.
Returns `true` if there was a refinement, `false` otherwise. Returns `true` if there was a refinement, `false` otherwise.
""" """
@skip_refinement_for [term(), dynamic()]
def refine_body_var(var_or_version, type, expr, stack, context)
def refine_body_var({_, meta, _}, type, expr, stack, context) do def refine_body_var({_, meta, _}, type, expr, stack, context) do
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context) version = Keyword.fetch!(meta, :version)
end
def refine_body_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context %{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
context = if gradual?(old_type) and type not in [term(), dynamic()] do
case context.conditional_vars do case compatible_intersection(old_type, type) do
%{} = conditional_vars -> {:ok, new_type} when new_type != old_type ->
%{context | conditional_vars: Map.put(conditional_vars, version, true)} data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
nil -> {new_type, %{context | vars: %{vars | version => data}}}
context
_ ->
{old_type, context}
end end
else
case stack do {old_type, context}
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context}
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
new_type = opt_intersection(old_type, type)
case empty?(new_type) do
true when reverse_arrow == :include_none ->
data = %{
data
| type: none(),
off_traces: new_trace(expr, none(), stack, off_traces)
}
{none(), %{context | vars: %{vars | version => data}}}
false when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
_ ->
case gradual?(old_type) and compatible_intersection(old_type, type) do
{:ok, new_type} when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
end end
end end
@@ -160,18 +81,13 @@ defmodule Module.Types.Of do
because we want to refine types. Otherwise we should because we want to refine types. Otherwise we should
use compatibility. use compatibility.
""" """
def refine_head_var({_, meta, _}, type, expr, stack, context) do def refine_head_var(var, type, expr, stack, context) do
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context) {var_name, meta, var_context} = var
end version = Keyword.fetch!(meta, :version)
def refine_head_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
case context.vars do case context.vars do
%{^version => %{errored: true}} ->
{:ok, error_type(), context}
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars -> %{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
new_type = opt_intersection(type, old_type) new_type = intersection(type, old_type)
data = %{ data = %{
data data
@@ -179,14 +95,26 @@ defmodule Module.Types.Of do
off_traces: new_trace(expr, type, stack, off_traces) off_traces: new_trace(expr, type, stack, off_traces)
} }
context = %{context | vars: %{vars | version => data}}
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do if empty?(new_type) do
data = Map.put(%{data | type: error_type()}, :errored, true) {:error, error_type(),
context = %{context | vars: %{vars | version => data}} error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
{:error, old_type, context}
else else
context = %{context | vars: %{vars | version => data}}
{:ok, new_type, context} {:ok, new_type, context}
end end
%{} = vars ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: new_trace(expr, type, stack, [])
}
context = %{context | vars: Map.put(vars, version, data)}
{:ok, type, context}
end end
end end
@@ -196,76 +124,16 @@ defmodule Module.Types.Of do
defp new_trace(expr, type, stack, traces), defp new_trace(expr, type, stack, traces),
do: [{expr, stack.file, type} | traces] do: [{expr, stack.file, type} | traces]
@doc """
Preserves `context` in first argument while
resetting it to the vars in the second argument.
"""
def reset_vars(context, %{
subpatterns: subpatterns,
vars: vars,
conditional_vars: conditional_vars
}),
do: %{context | subpatterns: subpatterns, vars: vars, conditional_vars: conditional_vars}
@doc """
Returns true if all entries have the same conditional vars.
"""
def all_same_conditional_vars?([{_, cond} | tail]) do
Enum.all?(tail, fn {_, tail_cond} -> cond == tail_cond end)
end
@doc """
Executes the args with acc using conditional variables.
"""
def with_conditional_vars(args, acc, expr, stack, context, fun) do
%{vars: vars, conditional_vars: conditional_vars} = context
{vars_conds, {acc, context}} =
Enum.map_reduce(args, {acc, context}, fn arg, {acc, context} ->
{acc, context} = fun.(arg, acc, %{context | vars: vars, conditional_vars: %{}})
%{vars: vars, conditional_vars: cond_vars} = context
{{vars, cond_vars}, {acc, context}}
end)
context = %{context | vars: vars, conditional_vars: conditional_vars}
{acc, reduce_conditional_vars(vars_conds, expr, stack, context)}
end
@doc """
Reduces conditional variables collected separately.
"""
def reduce_conditional_vars([{vars, cond} | vars_conds], expr, stack, context) do
%{vars: pre_vars} = context
Enum.reduce(Map.keys(cond), context, fn version, context ->
if is_map_key(pre_vars, version) and
Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
%{^version => %{type: type}} = vars
type =
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
%{^version => %{type: type}} = vars
opt_union(acc, type)
end)
{_, context} = refine_body_var(version, type, expr, stack, context)
context
else
context
end
end)
end
## Implementations ## Implementations
impls = [ impls = [
{Atom, atom()}, {Atom, atom()},
{BitString, bitstring()}, {BitString, binary()},
{Float, float()}, {Float, float()},
{Function, fun()}, {Function, fun()},
{Integer, integer()}, {Integer, integer()},
{List, opt_union(empty_list(), non_empty_list(term(), term()))}, {List, union(empty_list(), non_empty_list(term(), term()))},
{Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})}, {Map, open_map(__struct__: if_set(negation(atom())))},
{Port, port()}, {Port, port()},
{PID, pid()}, {PID, pid()},
{Reference, reference()}, {Reference, reference()},
@@ -294,7 +162,7 @@ defmodule Module.Types.Of do
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info) struct_type(struct, info)
else else
open_map(__struct__: {atom([struct]), false}) open_map(__struct__: atom([struct]))
end end
end end
@@ -304,7 +172,7 @@ defmodule Module.Types.Of do
Handles fetching a map key. Handles fetching a map key.
""" """
def map_fetch(expr, type, field, stack, context) when is_atom(field) do def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch_key(type, field) do case map_fetch(type, field) do
{_optional?, value_type} -> {_optional?, value_type} ->
{value_type, context} {value_type, context}
@@ -319,144 +187,107 @@ defmodule Module.Types.Of do
def closed_map(pairs, expected, stack, context, of_fun) do def closed_map(pairs, expected, stack, context, of_fun) do
{pairs_types, context} = pairs(pairs, expected, stack, context, of_fun) {pairs_types, context} = pairs(pairs, expected, stack, context, of_fun)
{dynamic?, domain, single, multiple} =
Enum.reduce(pairs_types, {false, [], [], []}, fn
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
dynamic? = dynamic? or dynamic_pair?
case pos_neg_domain do
# If atom is included in domain keys, it unions all previous
# single and multiple, except the ones negated:
#
# %{foo: :bar, term() => :baz}
# #=> %{foo: :bar or :baz, term() => :baz}
#
# %{foo: :bar, not :foo => :baz}
# #=> %{foo: :bar, term() => :baz}
#
# In case the negated term does not appear, we set it to none():
#
# %{foo: :bar, term() => :baz}
# #=> %{term() => :baz, foo: :bar or :baz}
#
# %{not :foo => :baz}
# #=> %{term() => :baz, foo: none()}
#
# In case we are dealing with multiple keys, we always merge the
# domain. A more precise approach would be to postpone doing so
# until the cartesian map is distributed but those should be very
# uncommon.
{[], negs, domain_keys} ->
if :atom in domain_keys do
{single, multiple} = union_negated(negs, value_type, single, multiple)
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
else
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
end
{pos, [], domain_keys} ->
domain =
case domain_keys do
[] -> domain
_ -> [{domain_keys, value_type} | domain]
end
case pos do
# Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples.
[key] when multiple == [] ->
{dynamic?, domain, [{key, {value_type, false}} | single], multiple}
_ ->
{dynamic?, domain, single, [{pos, value_type} | multiple]}
end
end
end)
non_multiple = Enum.reverse(single, domain)
map = map =
case Enum.reverse(multiple) do permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
[] -> # TODO: Use the fallback type to actually indicate if open or closed.
closed_map(non_multiple) if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
[{keys, type} | tail] ->
products = cartesian_map(tail)
for key <- keys, product <- products do
closed_map(non_multiple ++ [{key, {type, false}} | product])
end
|> Enum.reduce(&opt_union/2)
end
{if(dynamic?, do: dynamic(map), else: map), context}
end
defp union_negated([], new_type, single, multiple) do
single =
Enum.map(single, fn
{key, {old_type, optional?}} ->
{key, {opt_union(old_type, new_type), optional?}}
end) end)
multiple = {map, context}
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end)
{single, multiple}
end end
defp union_negated(negated, new_type, single, multiple) do @doc """
{single, matched} = Computes the types of key-value pairs.
Enum.map_reduce(single, [], fn """
{key, {old_type, optional?}}, matched -> def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
if key in negated do
{{key, {old_type, optional?}}, [key | matched]}
else
{{key, {opt_union(old_type, new_type), optional?}}, matched}
end
end)
multiple =
Enum.map(multiple, fn {keys, old_type} ->
{keys, opt_union(old_type, new_type)}
end)
{Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple}
end
defp pairs(pairs, expected, stack, context, of_fun) do
Enum.map_reduce(pairs, context, fn {key, value}, context -> Enum.map_reduce(pairs, context, fn {key, value}, context ->
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun) {_key_type, context} = of_fun.(key, term(), stack, context)
{value_type, context} = of_fun.(value, term(), stack, context)
{{true, :none, value_type}, context}
end)
end
def pairs(pairs, expected, stack, context, of_fun) do
Enum.map_reduce(pairs, context, fn {key, value}, context ->
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
expected_value_type = expected_value_type =
with {[key], [], []} <- pos_neg_domain, with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
{_optional?, expected_value_type} <- map_fetch_key(expected, key) do
expected_value_type expected_value_type
else else
_ -> term() _ -> term()
end end
{value_type, context} = of_fun.(value, expected_value_type, stack, context) {value_type, context} = of_fun.(value, expected_value_type, stack, context)
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context} {{dynamic_key? or gradual?(value_type), keys, value_type}, context}
end) end)
end end
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{{[key], [], []}, false, context} {false, [key], context}
end end
defp map_key_type(key, stack, context, of_fun) do defp finite_key_type(key, stack, context, of_fun) do
{key_type, context} = of_fun.(key, term(), stack, context) {key_type, context} = of_fun.(key, term(), stack, context)
domain_keys = to_domain_keys(key_type)
pos_neg_domain = case atom_fetch(key_type) do
case atom_fetch(key_type) do {:finite, list} -> {gradual?(key_type), list, context}
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)} _ -> {gradual?(key_type), :none, context}
{:infinite, list} -> {[], list, domain_keys} end
:error -> {[], [], domain_keys} end
@doc """
Builds permutation of maps according to the given pairs types.
"""
def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do
dynamic()
end
def permutate_map(pairs_types, _stack, of_map) do
{dynamic?, fallback, single, multiple, assert} =
Enum.reduce(pairs_types, {false, none(), [], [], []}, fn
{dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} ->
dynamic? = dynamic? or dynamic_pair?
case keys do
:none ->
fallback = union(fallback, value_type)
{fallback, assert} =
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
{union(fallback, type), [key | assert]}
end)
{fallback, assert} =
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
{union(fallback, type), keys ++ assert}
end)
{dynamic?, fallback, [], [], assert}
# Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples.
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert}
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert}
end
end)
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
end
|> Enum.reduce(&union/2)
end end
{pos_neg_domain, gradual?(key_type), context} if dynamic?, do: dynamic(map), else: map
end end
defp cartesian_map(lists) do defp cartesian_map(lists) do
@@ -465,66 +296,56 @@ defmodule Module.Types.Of do
[[]] [[]]
[{keys, type} | tail] -> [{keys, type} | tail] ->
products = cartesian_map(tail) for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
for key <- keys, product <- products, do: [{key, {type, false}} | product]
end end
end end
@doc """ @doc """
Handles instantiation of a new struct. Handles instantiation of a new struct.
This is expanded and validated by the compiler, so don't need to check the fields.
""" """
# TODO: Type check the fields match the struct # TODO: Type check the fields match the struct
def struct_instance(struct, args, expected, meta, stack, context, of_fun) def struct_instance(struct, args, expected, meta, %{mode: mode} = stack, context, of_fun)
when is_atom(struct) do when is_atom(struct) do
{info, context} = struct_info(struct, :expr, meta, stack, context, true) {_info, context} = struct_info(struct, meta, stack, context)
if is_nil(info) do # The compiler has already checked the keys are atoms and which ones are required.
{dynamic(), context} {args_types, context} =
else Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
# The compiler has already checked the keys are atoms and which ones are required. value_type =
{args_types, context} = with true <- mode != :traversal,
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> {_, expected_value_type} <- map_fetch(expected, key) do
value_type = expected_value_type
case map_fetch_key(expected, key) do else
{_optional?, expected_value_type} -> expected_value_type _ -> term()
_ -> term() end
end
{type, context} = of_fun.(value, value_type, stack, context) {type, context} = of_fun.(value, value_type, stack, context)
{{key, {type, false}}, context} {{key, type}, context}
end) end)
{closed_map([__struct__: {atom([struct]), false}] ++ args_types), context} {closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
end end
@doc """ @doc """
Returns `__info__(:struct)` information about a struct. Returns `__info__(:struct)` information about a struct.
""" """
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do def struct_info(struct, meta, stack, context) do
case stack.no_warn_undefined do case stack.no_warn_undefined do
%Macro.Env{} = env -> %Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
{:ok, info} -> {info, context} {:ok, info} -> {info, context}
{:error, _desc} -> {nil, context} {:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end end
_ -> _ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info = info =
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and struct.__info__(:struct) ||
struct.__info__(:struct) raise "expected #{inspect(struct)} to return struct metadata, but got none"
if info do {info, context}
{_, context} =
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
{info, context}
else
error = {:unknown_struct, kind, struct, must_exist?}
{nil, error(error, meta, stack, context)}
end
end end
end end
@@ -535,87 +356,51 @@ defmodule Module.Types.Of do
# we introduce typed structs. They are only used by exceptions. # we introduce typed structs. They are only used by exceptions.
def struct_type(struct, info, args_types \\ []) do def struct_type(struct, info, args_types \\ []) do
term = dynamic() term = dynamic()
pairs = for %{field: field} <- info, do: {field, {term, false}} pairs = for %{field: field} <- info, do: {field, term}
pairs = [{:__struct__, {atom([struct]), false}} | pairs] pairs = [{:__struct__, atom([struct])} | pairs]
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
pairs =
if args_types == [] do
pairs
else
pairs ++ args_types
end
closed_map(pairs) closed_map(pairs)
end end
@doc """ ## Binary
Returns shared error for unknown struct field.
"""
def unknown_struct_field(struct, field, kind, meta, stack, context) do
error = {:unknown_struct_field, kind, struct, field}
error(error, meta, stack, context)
end
## Bitstrings
@doc """ @doc """
Handles bitstrings. Handles binaries.
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc, In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary. based on the position of the expression within the binary.
""" """
def bitstring([], _kind, _stack, context) do def binary([], _kind, _stack, context) do
{binary(), context} context
end end
def bitstring([head], kind, stack, context) do def binary([head], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head], stack, context) binary_segment(head, kind, [head], stack, context)
{alignment_to_type(alignment), context}
end end
def bitstring([head | tail], kind, stack, context) do def binary([head | tail], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context) context = binary_segment(head, kind, [head, @suffix], stack, context)
bitstring_tail(tail, alignment, kind, stack, context) binary_many(tail, kind, stack, context)
end end
defp bitstring_tail([last], alignment, kind, stack, context) do defp binary_many([last], kind, stack, context) do
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context) binary_segment(last, kind, [@prefix, last], stack, context)
{alignment_to_type(alignment(seg_alignment, alignment)), context}
end end
defp bitstring_tail([head | tail], alignment, kind, stack, context) do defp binary_many([head | tail], kind, stack, context) do
{seg_alignment, context} = context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context) binary_many(tail, kind, stack, context)
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
end end
defp alignment(left, right) when is_integer(left) and is_integer(right), do: left + right
defp alignment(_left, _right), do: :unknown
defp alignment_to_type(:unknown), do: bitstring()
defp alignment_to_type(integer) when rem(integer, 8) == 0, do: binary()
defp alignment_to_type(_integer), do: bitstring_no_binary()
# If the segment is a literal, the compiler has already checked its validity, # If the segment is a literal, the compiler has already checked its validity,
# so we just check the size. # so we just skip it.
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context) defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
when is_binary(left) or is_number(left) do when is_binary(left) or is_number(left) do
{_type, alignment_type} = specifier_type(kind, right) context
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
# We don't need to check for bitstrings because the left side
# is either a binary (aligned), float (aligned), or integer
# (which we check below).
if alignment_type == :integer and alignment_value != :default do
{alignment_value, context}
else
{0, context}
end
end end
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
{type, alignment_type} = specifier_type(kind, right) type = specifier_type(kind, right)
expr = {:<<>>, meta, args} expr = {:<<>>, meta, args}
{actual, context} = {actual, context} =
@@ -632,26 +417,10 @@ defmodule Module.Types.Of do
end end
if compatible?(actual, type) do if compatible?(actual, type) do
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context}) specifier_size(kind, right, stack, context)
case alignment_type do
:aligned ->
{0, context}
:integer when alignment_value == :default ->
{0, context}
# There is no size, so the alignment depends on the type.
# If the type is exclusively a binary, then it is aligned.
:bitstring when alignment_value == :default ->
if bitstring_no_binary_type?(actual), do: {:unknown, context}, else: {0, context}
_ ->
{alignment_value, context}
end
else else
error = {:badbinary, kind, meta, expr, type, actual, context} error = {:badbinary, kind, meta, expr, type, actual, context}
{:unknown, error(error, meta, stack, context)} error(error, meta, stack, context)
end end
end end
@@ -667,48 +436,39 @@ defmodule Module.Types.Of do
end end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left) defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: {@integer, :aligned} defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned} defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned} defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned} defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned} defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned} defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned} defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned} defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer} defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring} defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring} defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned} defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned} defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: {@integer, :integer} defp specifier_type(_kind, _specifier), do: @integer
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context)) specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
end end
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context}) defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when is_integer(arg) do when not is_integer(arg) do
size = if unit == :default, do: arg, else: arg * unit
{size, context}
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, {_, context}) do
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context) {actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)} compatible_size(actual, expr, stack, context)
end end
defp specifier_size(_match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context) {actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)} compatible_size(actual, expr, stack, context)
end end
# We currently assume the unit always comes before size defp specifier_size(_kind, _specifier, _stack, context) do
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do context
{unit, context}
end
defp specifier_size(_kind, _specifier, _stack, align_context) do
align_context
end end
defp compatible_size(actual, expr, stack, context) do defp compatible_size(actual, expr, stack, context) do
@@ -729,12 +489,9 @@ defmodule Module.Types.Of do
""" """
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do case atom_fetch(type) do
{:finite, mods} -> {_, mods} ->
{mods, context} {mods, context}
{:infinite, _} ->
{[], context}
:error -> :error ->
warning = {:badmodule, expr, type, fun, arity, hints, context} warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)} {[], error(warning, meta, stack, context)}
@@ -747,6 +504,23 @@ defmodule Module.Types.Of do
error(__MODULE__, warning, meta, stack, context) error(__MODULE__, warning, meta, stack, context)
end end
def format_diagnostic({:refine_head_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)
])
}
end
def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do
type = if kind == :match, do: "matching", else: "construction" type = if kind == :match, do: "matching", else: "construction"
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: [] hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
@@ -866,37 +640,6 @@ defmodule Module.Types.Of do
} }
end end
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
detail =
case {Code.ensure_loaded?(module), must_exist?} do
{true, false} ->
"there is such module but it does not define a struct"
{false, false} ->
"module #{inspect(module)} is not available or is yet to be defined"
{true, true} ->
"the module may have been redefined as it no longer defines a struct"
{false, true} ->
"the module was also only available but may have been removed during compilation"
end
%{
message: "struct #{inspect(module)} is undefined (#{detail})",
group: true,
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
}
end
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
%{
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
}
end
defp dot_var?(expr) do defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr) match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end end

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