Compare commits

..
239 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
374 changed files with 11252 additions and 27028 deletions
+7 -12
View File
@@ -3,7 +3,8 @@
---
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:
- type: markdown
attributes:
@@ -12,17 +13,11 @@ body:
Please, do not use this form for guidance, questions or support.
Try instead in [Elixir Forum](https://elixirforum.com) or any of
our online communities (Slack, Discord, etc).
- type: checkboxes
id: existing-issue
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
Try instead in [Elixir Forum](https://elixirforum.com),
the [IRC Chat](https://web.libera.chat/#elixir),
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
[Slack](https://elixir-slackin.herokuapp.com),
[Discord](https://discord.gg/elixir) or in other online communities.
- type: textarea
id: elixir-and-otp-version
-2
View File
@@ -7,5 +7,3 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
+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-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: CI
on:
push:
paths-ignore:
- "lib/**/*.md"
pull_request:
workflow_dispatch:
paths-ignore:
- "lib/**/*.md"
env:
ELIXIR_ASSERT_TIMEOUT: 2000
@@ -19,64 +21,65 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
runs-on: ubuntu-24.04
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
strategy:
fail-fast: false
matrix:
include:
- otp_version: "29.0"
- otp_version: "28.1"
deterministic: true
- otp_version: "28.4"
docs: true
- otp_version: "28.1"
erlc_opts: "warnings_as_errors"
coverage: true
- otp_version: "28.1"
otp_latest: true
erlc_opts: "warnings_as_errors"
- otp_version: "27.3"
erlc_opts: "warnings_as_errors"
- otp_version: "27.0"
erlc_opts: "warnings_as_errors"
- otp_version: "26.0"
- otp_version: master
development: true
- otp_version: maint
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:
ERLC_OPTS: "warnings_as_errors"
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
with:
otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir
run: |
make compile
echo "$PWD/bin" >> $GITHUB_PATH
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
continue-on-error: ${{ matrix.development == true }}
continue-on-error: ${{ matrix.development }}
- name: Elixir test suite
run: make test_elixir
continue-on-error: ${{ matrix.development == true }}
continue-on-error: ${{ matrix.development }}
env:
COVER: "${{ matrix.coverage }}"
- name: "Calculate Coverage"
run: make cover | tee "$GITHUB_STEP_SUMMARY"
if: "${{ matrix.coverage }}"
- name: Build docs (ExDoc main)
if: ${{ matrix.docs }}
if: ${{ matrix.otp_latest }}
run: |
cd ..
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
@@ -85,67 +88,87 @@ jobs:
cd ../elixir/
git fetch --tags
DOCS_OPTIONS="--warnings-as-errors" make docs
- name: "Calculate Coverage"
if: ${{ matrix.coverage }}
run: make cover | tee "$GITHUB_STEP_SUMMARY"
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: |
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"
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: "${{ matrix.coverage }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: TestCoverage
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:
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
runs-on: windows-2022
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version:
- "29.0"
- "28.1"
- "27.3"
otp_version: ["26.2", "27.3", "28.1"]
runs-on: windows-2022
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
with:
otp-version: ${{ matrix.otp_version }}
- name: Compile Elixir
run: |
Remove-Item -Recurse -Force '.git'
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
- name: Elixir test suite
run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: "/language:${{matrix.language}}"
zizmor:
name: Zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
-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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Release Notifications
name: Notify
on:
release:
@@ -15,20 +15,17 @@ jobs:
notify:
runs-on: ubuntu-latest
name: Notify
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
with:
otp-version: "27.3"
elixir-version: "1.18.3"
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: |
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
elixir .github/workflows/notify.exs ${{ github.ref_name }}
-1
View File
@@ -46,7 +46,6 @@ runs:
repository: oss-review-toolkit/ort-config
ref: "main"
path: ".ort-config"
persist-credentials: false
- name: Setup ORT Config
id: setup-ort-config
-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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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-FileCopyrightText: 2021 The Elixir Team
name: Releases
name: Release
on:
push:
branches:
- main
- v*.*
tags:
- v*
workflow_dispatch:
env:
ELIXIR_OPTS: "--warnings-as-errors"
LANG: C.UTF-8
@@ -23,68 +20,64 @@ permissions:
jobs:
create_draft_release:
name: Create draft release
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo "$GITHUB_REPOSITORY" \
--title "$GITHUB_REF_NAME" \
--repo ${{ github.repository }} \
--title ${{ github.ref_name }} \
--notes '' \
--draft \
"$GITHUB_REF_NAME"
${{ github.ref_name }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# zizmor: ignore[artipacked]
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
if: github.ref_type == 'branch'
with:
fetch-depth: 50
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
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 \
--latest=false \
--title "$ref_name" \
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
"$ref_name"
--title $ref_name \
--notes "Automated release for latest ${{ github.ref_name }}." \
$ref_name
fi
git tag "$ref_name" --force
git push origin "$ref_name" --force
git tag $ref_name --force
git push origin $ref_name --force
build:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
runs-on: ubuntu-24.04
name: "Build Elixir"
strategy:
fail-fast: true
matrix:
include:
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
- otp: 28
otp_version: "28.0"
build_docs: build_docs
- otp: 29
otp_version: "29.0"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
fetch-depth: 50
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
@@ -99,39 +92,38 @@ jobs:
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: matrix.build_docs
with:
name: Docs
path: Docs.zip*
sign:
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [27, 28, 29]
otp: [26, 27, 28]
flavor: [windows, linux]
env:
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:
contents: write
@@ -139,20 +131,19 @@ jobs:
steps:
- name: "Download build"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
@@ -181,15 +172,17 @@ jobs:
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
sbom:
name: Generate SBoM
needs: [build, sign]
runs-on: ubuntu-24.04
permissions:
@@ -205,13 +198,11 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -226,7 +217,7 @@ jobs:
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -246,19 +237,15 @@ jobs:
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
env:
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"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: "SBoM"
path: |
@@ -268,38 +255,37 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: "Attestations"
path: "attestations/*.sigstore"
upload-release:
name: Upload release
needs: [create_draft_release, build, sign, sbom]
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
- name: Upload Pre-build
- name: Upload Pre-built
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
tag="${GITHUB_REF_NAME}-latest"
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
else
tag="$GITHUB_REF_NAME"
tag="${{ github.ref_name }}"
fi
gh release upload \
--repo "$GITHUB_REPOSITORY" \
--repo ${{ github.repository }} \
--clobber \
"$tag" \
elixir-otp-*.zip \
@@ -314,18 +300,15 @@ jobs:
bom.*
upload-builds-hex-pm:
name: Upload builds to hex.pm
runs-on: ubuntu-24.04
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps:
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
@@ -333,7 +316,7 @@ jobs:
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -344,10 +327,12 @@ jobs:
- name: Upload Precompiled to S3
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)
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$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
@@ -356,16 +341,16 @@ jobs:
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
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" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${ref_name} >> purge_keys.txt
fi
done
- name: Upload Docs to S3
run: |
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
unzip Docs.zip
@@ -386,7 +371,7 @@ jobs:
- name: Update builds txt
run: |
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)
@@ -439,7 +424,6 @@ jobs:
for key in $(cat purge_keys.txt); do
purge "${key}"
done
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_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-FileCopyrightText: 2021 The Elixir Team
name: Release Pre-build
description: "Builds Elixir release, ExDoc and generates docs"
name: "Release pre built"
description: "Builds elixir release, ExDoc and generates docs"
inputs:
otp:
description: "The major OTP version"
otp_version:
description: "The exact OTP version (major.minor[.patch])"
build_docs:
description: "Whether docs have to be built"
description: "If docs have to be built or not"
runs:
using: "composite"
steps:
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
with:
otp-version: ${{ inputs.otp_version }}
version-type: strict
- name: Build Elixir Release
shell: bash
run: | # zizmor: ignore[github-env]
run: |
make Precompiled.zip
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
echo "$PWD/bin" >> $GITHUB_PATH
env:
INPUT_OTP: ${{ inputs.otp }}
- name: Install NSIS
shell: bash
run: |
sudo apt update
sudo apt install -y nsis
- name: Build Elixir Windows Installer
shell: bash
run: |
export OTP_VERSION="$INPUT_OTP_VERSION"
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
export OTP_VERSION=${{ inputs.otp_version }}
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
env:
INPUT_OTP: ${{ inputs.otp }}
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
run: | # zizmor: ignore[github-env]
if [ "$GITHUB_REF_NAME" = "main" ]; then
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
ref=main
else
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
ref: ${{ env.EX_DOC_REF }}
path: ex_doc
persist-credentials: false
- name: Build ex_doc
if: ${{ inputs.build_docs }}
shell: bash
+2 -3
View File
@@ -10,10 +10,9 @@
/lib/elixir/test/ebin/
/man/elixir.1
/man/iex.1
/Docs.zip
/Precompiled.zip
/Docs-v*.zip
/Precompiled-v*.zip
/.eunit
.elixir.plt
erl_crash.dump
/cover/
.tool-versions
-62
View File
@@ -1,62 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
"globs": [
"**/*.md"
],
"ignores": [
".git/**"
],
"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
}
+438 -355
View File
@@ -4,428 +4,511 @@
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.20
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
# Changelog for Elixir v1.19
## Type system improvements
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
### Type checking of protocol dispatch and implementations
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
This release also adds type checking when dispatching and implementing protocols.
### Type inference of guards
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.
This release also performs inference of guards! Let's see some examples:
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
```elixir
def example(x, y) when is_list(x) and is_integer(y)
```
The code above correctly infers `x` is a list and `y` is an integer.
```elixir
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
```
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
```elixir
def example(x) when is_map_key(x, :foo)
```
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
```elixir
def example(x) when not is_map_key(x, :foo)
```
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
You can also have expressions that assert on the size of data structures:
```elixir
def example(x) when tuple_size(x) < 3
```
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
### Whole-body type inference
Elixir also performs inference based on the function body itself. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
### Typing across clauses
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
```elixir
case System.get_env("SOME_VAR") do
nil -> :not_found
value -> {:ok, String.upcase(value)}
end
```
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
### Typing of atom and domain keys in maps
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
```elixir
%{123 => "hello", 456.0 => :ok}
```
will have the type:
```elixir
%{integer() => binary(), float() => :ok}
```
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
```elixir
%{integer() => integer(), root: integer()}
```
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
### Typing of map operations
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
```elixir
Map.put(map, :key, 123)
#=> returns type %{..., key: integer()}
Map.delete(map, :key)
#=> returns type %{..., key: not_set()}
```
As you can see, we track when keys are set and also when they are removed.
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
```elixir
Map.replace(map, :key, 123)
#=> returns type %{..., key: if_set(integer())}
```
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
```elixir
defmodule User do
def name(map), do: Map.fetch!(map, :name)
end
defmodule CallsUser do
def calls_name do
User.name(%{})
defmodule Example do
def my_code(first..last//step = range) do
"hello #{range}"
end
end
```
The code above has a type violation, which is now caught by the type system:
the above emits the following warnings:
```text
warning: incompatible types given to User.name/1:
```
warning: incompatible value given to string interpolation:
User.name(%{})
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:
%{name: not_set()}
binary()
but expected one of:
but function has type:
dynamic(%{..., name: term()})
(dynamic(map()) -> :map)
type warning found at:
typing violation found at:
│
16 │ User.name(%{})
│ ~
6 │ fun.("hello")
│ ~
│
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
└─ 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/) and [Tidewave](https://tidewave.ai/).
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/).
## Compile-time improvements
## Faster compile times in large projects
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
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.
* Errors during compilation may have less precise stacktraces
### Code loading bottlenecks
* Anonymous functions within `defmodule` can have only up to 20 arguments.
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
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.
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
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.
## v1.20.4 (2026-08-28)
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)
### 2. Bug fixes
#### Elixir
* [Base] Fix false positives in Base64 validation
* [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring
* [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression
* [String] Fix final sigma handling in Greek casing
* [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count
## v1.20.3 (2026-08-04)
## v1.19.5 (2026-01-09)
### 1. Enhancements
#### Elixir
* [Kernel.ParallelCompiler] Batch type checker cache operations to improve compilation times
* [Protocol] Optimize protocol consolidation to no longer load structs
### 2. Bug fixes
#### Elixir
* [Kernel] Improve type precision of the `++/2` operator
* [Kernel] Improve type precision of the `send/2` return type
* [Kernel] Improve type refinement for `and/2` and `or/2`
* [Kernel] Fix type checking of `Map.put/3` on empty map types
* [Kernel] Fix type checking of tuple insertion on equivalent tuple types
* [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts
* [Kernel] Fix variable refinements leaking from invalid `receive/after` timeout expressions
* [Kernel] Fix rendering inferred types containing unions of static and dynamic functions
* [Kernel] Ensure typing errors in `:maps.values/1` reference the correct function
* [Kernel] Mark `%_{}` patterns as precise so subsequent redundant struct clauses are detected
* [elixirc] Fix parsing of the `--profile time` option
* [Kernel] Fix unnecessary recompilation when `dbg_callback` is modified at runtime
* [Kernel] Fix parser crash on missing parentheses on expression following operator `not in`
* [Kernel] Support fetching abstract code for modules compiled with Elixir v1.14 and earlier
* [Protocol] Ensure protocol consolidation no longer stores outdated struct types. As a consequence, protocols types only track struct names at the moment
* [Stream] Revert optimization which caused nested streams in `Stream.flat_map/2` to crash
#### IEx
* [IEx.Helpers] Fix `break!/1` with unknown expressions
## v1.20.2 (2026-06-23)
### 1. Enhancements
#### Elixir
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
### 2. Bug fixes
#### Elixir
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
* [Module] Fix type checking when applying an empty function type
* [Module] Fix type checking of bitstring patterns that reuse variables
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
* [Module] Fix typing of list types involving dynamic or empty lists
* [Module] Include bitstrings as a possible domain key in the type system
* [Module] Preserve file metadata from each clause in type system warnings
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
#### Mix
* [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
* [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
## v1.20.1 (2026-06-09)
### 1. Security
#### Elixir
* [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)
### 2. Bug fixes
#### Elixir
* [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters
* [Code] Ensure `Code.require_file` releases the file if compilation fails
* [Kernel] Fix documentation generation to use the correct version in search
#### Mix
* [mix archive.install] Validate paths and files when extracting archives
* [mix format] Honor `--no-compile` option when loading plugins
## v1.20.0 (2026-06-03)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### EEx
* [EEx] Optimize compiler by flattening expr list only once
#### Elixir
* [Base] Optimize Base validation functions by using SWAR techniques
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Code] Add `:dbg_callback` option to eval functions
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [Enum] Add `Enum.min_max` sorter
* [File] Add support for `[:raw]` opts in `File.read/2`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Float] Optimize `Float.round/2` by avoiding big integers
* [Inspect] Increase inspect limit to help print deeply nested data structures
* [Inspect] Support printing Erlang records (using Erlang notation)
* [Integer] Add `Integer.ceil_div/2`
* [Integer] Add `Integer.popcount/1`
* [IO] Add `IO.iodata_empty?/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Kernel] Detect and warn on redundant clauses
* [Kernel] Perform type inference across applications
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Kernel] Warn on unused requires
* [List] Add `List.first!/1` and `List.last!/1`
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
* Add Software Bill of Materials guide to the Documentation
#### ExUnit
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### IEx
* [IEx] Optimize autocompleting modules
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix app.tree] Support `--output` option
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
* [mix deps] Support filtering `mix deps` output
* [mix deps.tree] Support `--output` option
* [mix format] Support `--no-compile` option
* [mix help] Support printing docs for types and callbacks
* [mix source] Add `mix source MODULE` to print or open a given module/function location
* [mix test] Add `mix test --dry-run`
### 2. Potential breaking changes
#### Elixir
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
### 3. Bug fixes
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
* [File] Warn when defining `@type record()` for Erlang/OTP 29
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
* [Kernel] Reject negative Duration in `to_timeout/1`
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
* [String] Support empty pattern list in `String.count/2`
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
#### IEx
* [IEx] Ensure pry works across remote nodes
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
* [IEx] Fix usage of `#iex:break` as part of multi-line prompts
#### Logger
* [Logger] Persist log level to app env in `Logger.configure/1`
* [Logger.Backends] Do not crash on invalid metadata
## v1.19.4 (2025-11-27)
### 1. Enhancements
#### Mix
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile] Add a build lock around protocol consolidation in umbrellas
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
* [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
### 2. Bug fixes
#### Elixir
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
#### 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
* [Kernel] Support /E modifier for regular expressions in config files
#### Mix
* [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
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
* [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] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
* [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.19
## v1.18
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
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
Contact: <elixir-lang-conduct@googlegroups.com>
Contact: elixir-lang-conduct@googlegroups.com
## 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:
* The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
* The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists.
* 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.
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.
+45 -96
View File
@@ -78,7 +78,6 @@ introduced behavior, especially for bug fixes and major changes:
*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
major changes to existing functionality, please add tests that cover the
major parts of that functionality. Aim to have the best code coverage possible.
@@ -89,9 +88,7 @@ We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes
@@ -99,11 +96,15 @@ case you are looking for some examples:
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be reviewed before being merged into 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.
All pull requests must be approved by two committers before being merged into
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
@@ -114,63 +115,36 @@ into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built 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://hexdocs.pm/elixir/writing-documentation.html).
## Licensing and Compliance Requirements
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
affecting **all external contributors**:
* Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
* SPDX License Headers: With the exception of approved test fixture files,
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
instance, please use:
- Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
- SPDX License Headers: With the exception of approved test fixture files,
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
instance, please use:
```elixir
# SPDX-License-Identifier: Apache-2.0
# 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:
(a) The contribution was created in whole or in part by me and I
@@ -197,52 +171,27 @@ affecting **all external contributors**:
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.
## Using AI and coding agents
## Building documentation
While we allow the use of AI on contributions and discussions, please be mindful
when doing so. Generally speaking, Elixir maintainers already have access to AI
(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.
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
That said, here are examples of how one might (or might not) use AI and coding
agents in Elixir spaces:
```sh
# 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,
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.
Now go back to Elixir's root directory and run:
* Do not use coding agents to tackle existing issues unless they have the
"Contributions Welcome" label.
```sh
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
use coding agents to implement it, as long as it follows the AI Contributions
guidelines below.
* When automating AI usage on the Elixir codebase for performance improvements
or security fixes, 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
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://hexdocs.pm/elixir/writing-documentation.html).
+3 -5
View File
@@ -32,9 +32,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 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 \
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; \
fi
endef
@@ -107,10 +107,8 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
fi
@ echo "==> elixir (compile)";
$(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)
unicode: $(UNICODE)
+13 -10
View File
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
## 2. Scope
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.
## 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
licenses are recognized as permissible in this project:
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
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
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
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
@@ -52,9 +51,13 @@ configuration and undergo review.
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
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
be added. This practice ensures that original authors receive proper credit and
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.
*Effective Date: 2025-02-20*
*Last Reviewed: 2025-11-20*
*Last Reviewed: 2025-02-20*
+4 -5
View File
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
comment and we can always reopen the issue.
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
remember to keep the tone positive and be kind! For more information,
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
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
code, and submit pull requests.
Note you must disclose the use of coding agents and AI written code in your
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
code, and submit pull requests. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
contribution experience.
## 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`
## Creating a new vMAJOR.MINOR branch (usually before first rc)
## Creating a new vMAJOR.MINOR branch (before first rc)
### In the new branch
@@ -36,7 +36,7 @@
### 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
+3 -3
View File
@@ -12,15 +12,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.19 | Bug fixes and security patches
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
## 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.
+1 -1
View File
@@ -1 +1 @@
1.20.4
1.19.6
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.4
ELIXIR_VERSION=1.19.6
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.20.4
set ELIXIR_VERSION=1.19.6
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+1 -2
View File
@@ -293,7 +293,7 @@ defmodule EEx do
"foo baz"
"""
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
@spec eval_string(String.t(), keyword, [compile_opt]) :: String.t()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options)
@@ -364,7 +364,6 @@ defmodule EEx do
### Helpers
defp do_eval(compiled, bindings, options) do
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
{result, _} = Code.eval_quoted(compiled, bindings, options)
result
end
+14 -27
View File
@@ -96,7 +96,7 @@ defmodule EEx.Compiler do
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
"please remove \"#{marker}\""
IO.warn(message, file: state.file, line: line, column: column)
:elixir_errors.erl_warn({line, column}, state.file, message)
~c""
else
marker
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
file: file,
source: source,
line: line,
quoted: %{},
quoted: [],
parser_options: [indentation: indentation] ++ parser_options,
indentation: indentation
}
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
state.parser_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)
end
@@ -366,17 +366,17 @@ defmodule EEx.Compiler do
rest,
state.engine.handle_begin(buffer),
[{contents, start_line, start_column} | scope],
%{state | quoted: %{}, line: line}
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
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)
end
@@ -410,7 +410,7 @@ defmodule EEx.Compiler do
) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
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, rest}
end
@@ -426,7 +426,7 @@ defmodule EEx.Compiler do
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
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)
end
@@ -443,10 +443,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line)
key = map_size(state.quoted)
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
count = [current, placeholder, new_lines, chars]
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
key = length(state.quoted)
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
{count, new_state}
end
@@ -479,7 +479,8 @@ defmodule EEx.Compiler do
# Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
Map.fetch!(quoted, key)
{^key, value} = List.keyfind(quoted, key, 0)
value
end
defp insert_quoted({left, line, right}, quoted) do
@@ -512,20 +513,6 @@ defmodule EEx.Compiler do
column: meta.column
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
line_start = max(meta.line - 3, 1)
line_end = meta.line
-12
View File
@@ -575,18 +575,6 @@ defmodule EExTest do
EEx.compile_string("foo <%= bar", file: "my_file.eex")
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
describe "warnings" do
+5 -10
View File
@@ -8,13 +8,8 @@
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("eex")
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: []
ex_unit_opts =
[
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
] ++ maybe_seed_opt
ExUnit.start(ex_unit_opts)
ExUnit.start(
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
)
+10 -5
View File
@@ -873,6 +873,11 @@ defmodule Access do
...> end)
{[], [%{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:
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
@@ -1091,11 +1096,6 @@ defmodule Access do
defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
when index > last do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
if index in range do
case next.(head) do
@@ -1154,6 +1154,11 @@ defmodule Access do
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
+13 -19
View File
@@ -507,7 +507,7 @@ defmodule Application do
of all loaded applications. Returns `nil` if
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
case :application.get_application(module) do
{:ok, app} -> app
@@ -680,13 +680,13 @@ defmodule Application do
## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications.
Since Mix configurations are commonly used to configure applications (including
your dependencies), we will use this as a point of illustration.
Since Mix configurations are commonly used to configure applications, we will use
this as a point of illustration.
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
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,
# A database configuration
@@ -696,7 +696,7 @@ defmodule Application do
config :my_app, Databases.RepoTwo,
# Another database configuration (for the same OTP app)
ip: "localhost",
port: 20_717
port: 20717
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[: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
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.
"""
@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)
:application.set_env(app, key, value, opts)
end
@@ -861,7 +856,7 @@ defmodule Application do
It receives the same options as `put_env/4`. Returns `: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)
:application.unset_env(app, key, opts)
end
@@ -908,13 +903,13 @@ defmodule Application do
@doc """
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.
## Options
* `: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
(`:serial`, default) or concurrently (`:concurrent`).
@@ -926,11 +921,11 @@ defmodule Application do
{:ok, [app]} | {:error, term}
def ensure_all_started(app_or_apps, type_or_opts \\ [])
def ensure_all_started(app_or_apps, type) when is_atom(type) do
ensure_all_started(app_or_apps, type: type)
def ensure_all_started(app, type) when is_atom(type) do
ensure_all_started(app, type: type)
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)
end
@@ -1061,8 +1056,7 @@ defmodule Application do
Returns a list with information about the applications which are currently running.
"""
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
def started_applications(timeout \\ 5000)
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
def started_applications(timeout \\ 5000) do
:application.which_applications(timeout)
end
+54 -278
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>>
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 """
Encodes a binary string into a base 16 encoded string.
@@ -620,21 +371,25 @@ defmodule Base do
decode_name = :"decode16#{base}!"
validate_name = :"validate16#{base}?"
valid_char_name = :"valid_char16#{base}?"
valid_word_name = :"valid_word16#{base}?"
{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)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
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
index = char - unquote(min)
@@ -1006,19 +761,23 @@ defmodule Base do
validate_name = :"validate64#{base}?"
validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = :"valid_char64#{base}?"
valid_word_name = :"valid_word64#{base}?"
{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)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(
<<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
@@ -1104,6 +863,13 @@ defmodule Base do
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
index = char - unquote(min)
@@ -1659,18 +1425,21 @@ defmodule Base do
valid_char_name = :"valid_char32#{base}?"
{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 = :"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)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(
<<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
@@ -1750,6 +1519,13 @@ defmodule Base do
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
index = char - unquote(min)
+2 -9
View File
@@ -3,8 +3,6 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar do
@strftime_max_width 1024
@moduledoc """
This module defines the responsibilities for working with
calendars, dates, times and datetimes in Elixir.
@@ -531,7 +529,6 @@ defmodule Calendar do
* `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section
(maximum #{@strftime_max_width})
* `<format>`: the format itself (see below)
### Accepted padding options
@@ -670,13 +667,9 @@ defmodule Calendar do
end
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
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
end
parse_modifiers(rest, width, pad, parser_data)
parse_modifiers(rest, new_width, pad, parser_data)
end
# set default padding if none was specified
+1 -1
View File
@@ -53,7 +53,7 @@ defmodule Date do
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
14716
iex> Date.add(~D[1970-01-01], 14_716)
iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
+7 -16
View File
@@ -59,20 +59,11 @@ defmodule Date.Range do
end
# TODO: Remove me on v2.0
member? =
quote generated: true do
member?(
%{
__struct__: Date.Range,
first_in_iso_days: var!(first_days),
last_in_iso_days: var!(last_days)
} =
var!(date_range),
var!(date)
)
end
def unquote(member?) do
def member?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range,
date
) do
step = if first_days <= last_days, do: 1, else: -1
member?(Map.put(date_range, :step, step), date)
end
@@ -227,11 +218,11 @@ defmodule Date.Range do
defimpl Inspect do
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) <> ")"
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})"
end
+2 -3
View File
@@ -1622,9 +1622,8 @@ defmodule DateTime do
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function relies on a contiguous representation of time,
ignoring timezone changes. For example, if you add one day when there
+2 -2
View File
@@ -232,7 +232,7 @@ defmodule Duration do
@doc """
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
@@ -262,7 +262,7 @@ defmodule Duration do
@doc """
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
+153 -83
View File
@@ -182,7 +182,7 @@ defmodule Calendar.ISO do
@type day_of_year :: 1..366
@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_hour 60 * 60
@@ -196,28 +196,14 @@ defmodule Calendar.ISO do
@ext_date_sep ?-
@ext_time_sep ?:
@days_per_nonleap_year 365
@days_per_leap_year 366
# The ISO epoch starts, in this implementation,
# with ~D[0000-01-01]. Era "1" starts
# on ~D[0001-01-01] which is 366 days later.
@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] =
quote do
[
@@ -666,12 +652,12 @@ defmodule Calendar.ISO do
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
{{year, month, day}, {hour, minute, second, _}} =
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
{0, day_fraction} ->
{{year, month, day}, time_from_day_fraction(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)}
end
@@ -798,13 +784,13 @@ defmodule Calendar.ISO do
## 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}}
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}}
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}}
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}}
"""
@@ -892,52 +878,28 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days(year, month, day) do
ensure_day_in_month!(year, month, day)
valid_date_to_iso_days(year, month, day)
def date_to_iso_days(0, 1, 1) do
0
end
defp valid_date_to_iso_days(0, 1, 1), do: 0
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
def date_to_iso_days(1970, 1, 1) do
719_528
end
defp valid_date_to_iso_days(year, month, day) do
y = if month <= 2, do: year - 1, else: year
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
def date_to_iso_days(year, month, day) do
ensure_day_in_month!(year, month, day)
day_of_era =
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
era * @days_per_era + day_of_era + @march_1_offset
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
1
end
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
z = days - @march_1_offset
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
day_of_era = z - era * @days_per_era
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}
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
{year, month, day_in_month + 1}
end
defp div_rem(int1, int2) do
@@ -951,9 +913,6 @@ defmodule Calendar.ISO do
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 """
Returns how many days there are in the given year-month.
@@ -1174,7 +1133,7 @@ defmodule Calendar.ISO do
"""
@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_BCE(year), do: {abs(year) + 1, 0}
@@ -1200,7 +1159,7 @@ defmodule Calendar.ISO do
"""
@doc since: "1.13.0"
@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)
@doc """
@@ -1745,11 +1704,11 @@ defmodule Calendar.ISO do
## 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}}
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}}
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}}
"""
@@ -1765,11 +1724,11 @@ defmodule Calendar.ISO do
## 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}}
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}}
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}}
"""
@@ -1878,7 +1837,7 @@ defmodule Calendar.ISO do
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
valid_date_to_iso_days(year, month, day)
date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
@@ -1889,7 +1848,7 @@ defmodule Calendar.ISO do
months_in_year = 12
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 =
case rem(total_months, months_in_year) + 1 do
@@ -1929,7 +1888,7 @@ defmodule Calendar.ISO do
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86_400, :second, unit)
ppd = System.convert_time_unit(86400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
@@ -1978,7 +1937,7 @@ defmodule Calendar.ISO do
}) do
[
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
]
end
@@ -2012,7 +1971,7 @@ defmodule Calendar.ISO do
if total in @unix_range_microseconds do
microseconds = Integer.mod(total, @microseconds_per_second)
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
precision = precision_for_unit(unit)
{date, time} = iso_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}}
@@ -2134,12 +2093,9 @@ defmodule Calendar.ISO do
end
end
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
# 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,
# add the result of leap_day_offset/2 to the result of days_before_month/1.
defp days_before_month(1), do: 0
defp days_before_month(2), do: 31
defp days_before_month(3), do: 59
@@ -2153,6 +2109,120 @@ defmodule Calendar.ISO do
defp days_before_month(11), do: 304
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
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
+32 -39
View File
@@ -402,9 +402,8 @@ defmodule NaiveDateTime do
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
## Examples
@@ -715,18 +714,16 @@ defmodule NaiveDateTime do
"""
@spec to_date(Calendar.naive_datetime()) :: Date.t()
def to_date(
%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
} = _naive_datetime
) do
def to_date(%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
}) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@@ -743,18 +740,16 @@ defmodule NaiveDateTime do
"""
@spec to_time(Calendar.naive_datetime()) :: Time.t()
def to_time(
%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = _naive_datetime
) do
def to_time(%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
%Time{
hour: hour,
minute: minute,
@@ -1150,18 +1145,16 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(
%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
} = _naive_datetime
) do
def to_gregorian_seconds(%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
+2 -3
View File
@@ -514,9 +514,8 @@ defmodule Time do
Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
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.
+72 -123
View File
@@ -286,25 +286,19 @@ defmodule Code do
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
static_atoms_encoder: (atom() -> term()),
emit_warnings: boolean()
]
@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 ::
{:file, binary()}
| {:line, pos_integer()}
| {:module, module()}
@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()}}
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@boolean_compiler_options [
:docs,
@@ -318,12 +312,7 @@ defmodule Code do
@available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++
[
:on_undefined_variable,
:infer_signatures,
:no_warn_undefined,
:module_definition
]
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
@doc """
Lists all required files.
@@ -579,11 +568,9 @@ defmodule Code do
## Options
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
as third argument, so the evaluation happens within that environment.
## Return
It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -613,11 +600,11 @@ defmodule Code do
iex> Enum.sort(binding)
[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
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
3
@@ -625,28 +612,21 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_string(string, binding \\ [], opts_or_env \\ [])
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
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
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
defp validate_binding(binding) when is_list(binding), do: binding
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
defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
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}
end
@@ -1149,8 +1129,7 @@ defmodule Code do
returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`.
The options are described under `env_for_eval/1`.
## Examples
@@ -1172,20 +1151,11 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{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}
end
@@ -1213,9 +1183,14 @@ defmodule Code do
* `: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"
@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)
@doc """
@@ -1229,19 +1204,11 @@ defmodule Code do
## Options
* `: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.
* `: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.
It accepts the same options as `env_for_eval/1`.
"""
@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()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1319,7 +1286,7 @@ defmodule Code do
and keyword lists.
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}`.
The encoder function is supposed to create an atom from the given
@@ -1358,7 +1325,14 @@ defmodule Code do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 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
@doc """
@@ -1434,7 +1408,8 @@ defmodule Code do
Process.put(:code_formatter_comments, [])
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))
{:ok, forms, comments}
end
@@ -1634,19 +1609,13 @@ defmodule Code do
nil
:proceed ->
try do
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
:elixir_code_server.cast({:required, file})
loaded
catch
kind, reason ->
:elixir_code_server.call({:release, file})
:erlang.raise(kind, reason, __STACKTRACE__)
end
:elixir_code_server.cast({:required, file})
loaded
end
end
@@ -1741,6 +1710,9 @@ defmodule Code do
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
module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`.
@@ -1756,9 +1728,6 @@ defmodule Code do
via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files.
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `: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.
Defaults to `false`.
@@ -1770,37 +1739,24 @@ defmodule Code do
should be using during type inference. When `false`, it disables module-local
signature inference used when type checking remote calls to the compiled
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
it will disable this option during test (as there is typically no need to infer
signature for test files). Outside of Mix projects, it defaults to `[:elixir]`.
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
* `:module_definition` (since v1.20.0) - stores if the module definition should
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
`.beam` file written to disk, only how the contents inside `defmodule` are
executed. Using the `:interpreted` mode may offer better compilation times for
large projects, especially on machines with high core count, however, it comes
with some downsides:
When setting this option, we recommend running `mix clean` so the current module
may be compiled from scratch. `mix test` automatically disables this option via
the `:test_elixirc_options` project configuration, as there is typically no need
to infer signatures for test files.
* Errors during compilation may have less precise stacktraces
* Anonymous functions within `defmodule` can have only up to 20 arguments.
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
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* `: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
at compilation time. Pass atom `:all` to skip warning for all undefined
functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
compilation. See the module docs for more information. Defaults to `[]`.
* `: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
@@ -1811,12 +1767,14 @@ defmodule Code do
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
latter is used for other purposes beyond compilation.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
compilation. See the module docs for more information. Defaults to `[]`.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
It always returns `:ok`. Raises an error for invalid options.
@@ -1856,15 +1814,6 @@ defmodule Code do
:ok
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
value =
cond do
+5 -40
View File
@@ -204,7 +204,6 @@ defmodule Code.Fragment do
| {:local_arity, charlist}
| {:local_call, charlist}
| {:anonymous_call, inside_caller}
| {:capture_arg, charlist}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:operator_arity, charlist}
@@ -1235,43 +1234,14 @@ defmodule Code.Fragment do
the cursor. This is necessary to correctly complete anonymous functions
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"
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{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 = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1291,10 +1261,7 @@ defmodule Code.Fragment do
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
@@ -1302,7 +1269,7 @@ defmodule Code.Fragment do
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
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} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
@@ -1321,12 +1288,10 @@ defmodule Code.Fragment do
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir_tokenizer.format_error(info)}
{:error, :elixir.format_token_error(info)}
end
end
+9 -16
View File
@@ -68,7 +68,7 @@ defmodule Code.Normalizer do
# Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state, false)
normalize_bitstring(quoted, state)
end
# Atoms with interpolations
@@ -89,7 +89,13 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state)
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]}
end
@@ -112,7 +118,6 @@ defmodule Code.Normalizer do
end
end)
parts = maybe_add_trailing_newline(call_meta, parts, state)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
@@ -400,8 +405,7 @@ defmodule Code.Normalizer do
defp allow_keyword?(:{}, _), do: false
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
parts = maybe_add_trailing_newline(meta, parts, state)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
meta = patch_meta_line(meta, state.parent_meta)
parts =
@@ -423,17 +427,6 @@ defmodule Code.Normalizer do
{:<<>>, meta, parts}
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
Enum.map(parts, fn
{:"::", interpolation_meta,
-16
View File
@@ -69,22 +69,6 @@ defprotocol Collectable do
iex> Enum.into([1, 2, 3], MapSet.new())
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
+7 -13
View File
@@ -377,27 +377,21 @@ defmodule Config do
end
end
defp validate!(config, file) when is_list(config) do
Enum.each(config, fn
defp validate!(config, file) do
Enum.all?(config, fn
{app, value} when is_atom(app) ->
if not Keyword.keyword?(value) do
if Keyword.keyword?(value) do
true
else
raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}"
end
other ->
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
_ ->
false
end)
config
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
+4 -5
View File
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
## 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 = [
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
@@ -206,7 +206,6 @@ defmodule DynamicSupervisor do
See `Supervisor` for more information about child specifications.
"""
@doc since: "1.6.1"
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
def child_spec(options) when is_list(options) do
id =
case Keyword.get(options, :name, DynamicSupervisor) do
@@ -481,11 +480,11 @@ defmodule DynamicSupervisor do
end
@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
of children under low memory conditions can bring the system down due to an
out of memory error.
of children under low memory conditions can cause an out of memory
exception.
This function returns a list of tuples containing:
+54 -165
View File
@@ -39,20 +39,6 @@ defprotocol Enumerable do
`reduce/3` function. All other functions exist as optimizations paths
for data structures that can implement certain properties in better
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 """
@@ -780,10 +766,6 @@ defmodule Enum do
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 """
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
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """
Enumerates the `enumerable`, returning a list where all consecutive
duplicate elements are collapsed to a single element.
@@ -829,7 +807,7 @@ defmodule Enum do
"""
@spec dedup(t) :: list
def dedup(enumerable) when is_list(enumerable) do
dedup_list(enumerable)
dedup_list(enumerable, []) |> :lists.reverse()
end
def dedup(enumerable) do
@@ -973,8 +951,8 @@ defmodule Enum do
## Examples
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
some
example
"some"
"example"
#=> :ok
"""
@@ -1236,7 +1214,7 @@ defmodule Enum do
end
@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`
on each element of `enumerable` together; conceptually, this is similar to a
@@ -1253,7 +1231,7 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[: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:
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
@@ -1284,16 +1262,13 @@ defmodule Enum do
defp flat_reverse([], acc), do: acc
@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
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
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
iex> enumerable = 1..100
@@ -1518,14 +1493,6 @@ defmodule Enum do
to_list(enumerable)
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
into_protocol(enumerable, collectable)
end
@@ -1602,12 +1569,8 @@ defmodule Enum do
map(enumerable, transform)
end
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do
if MapSet.size(collectable) == 0 do
MapSet.new(enumerable, transform)
else
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
end
def into(%_{} = enumerable, collectable, transform) do
into_protocol(enumerable, collectable, transform)
end
def into(enumerable, %_{} = collectable, transform) do
@@ -1879,7 +1842,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` according
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
was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true
@@ -1946,7 +1909,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` as calculated
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
was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true
@@ -2023,16 +1986,11 @@ defmodule Enum do
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) do
__in__(element, enumerable)
end
@doc false
def __in__(element, enumerable) when is_list(enumerable) do
def member?(enumerable, element) when is_list(enumerable) do
:lists.member(element, enumerable)
end
def __in__(element, enumerable) do
def member?(enumerable, element) do
case Enumerable.member?(enumerable, element) do
{:ok, element} when is_boolean(element) ->
element
@@ -2064,7 +2022,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` according
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
was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true
@@ -2131,7 +2089,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` as calculated
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
was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true
@@ -2185,60 +2143,28 @@ defmodule Enum do
@doc """
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,
as the function must not return true for equal elements.
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
Calls the provided `empty_fallback` function and returns its value if
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
## Examples
iex> Enum.min_max([2, 3, 1])
{1, 3}
iex> Enum.min_max(["foo", "bar", "baz"])
{"bar", "foo"}
iex> Enum.min_max([], fn -> {nil, nil} end)
{nil, nil}
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
iex> 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()) :: {element, element}
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
{element, element} | empty_result
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
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(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
case Range.size(range) do
0 ->
empty_fallback.()
@@ -2249,39 +2175,11 @@ defmodule Enum do
end
end
def min_max(enumerable, empty_fallback)
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
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
first_fun = &[&1 | &1]
reduce_fun = fn entry, [min | max] = acc ->
cond do
sorter.(entry, min) ->
[entry | max]
sorter.(max, entry) ->
[min | entry]
true ->
acc
end
reduce_fun = fn entry, [min | max] ->
[Kernel.min(min, entry) | Kernel.max(max, entry)]
end
case reduce_by(enumerable, first_fun, reduce_fun) do
@@ -2302,8 +2200,8 @@ defmodule Enum do
Returns a tuple with the minimal and the maximal elements in the
enumerable as calculated by the given function.
By default, the comparison is done with the [`<`](`</2`) sorter function,
as the function must not return `true` for equal elements.
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
## Examples
@@ -2361,7 +2259,7 @@ defmodule Enum do
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
end
def min_max_by(enumerable, fun, sorter, empty_fallback)
@@ -2392,7 +2290,7 @@ defmodule Enum do
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)
@doc """
Splits the `enumerable` in two lists according to the given function `fun`.
@@ -4196,11 +4094,6 @@ defmodule Enum do
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
[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"
@spec zip_with(t, ([term] -> term)) :: [term]
@@ -4512,9 +4405,19 @@ defmodule Enum do
# dedup
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
defp dedup_list([]), do: []
defp dedup_list([value | tail], acc) do
acc =
case acc do
[^value | _] -> acc
_ -> [value | acc]
end
dedup_list(tail, acc)
end
defp dedup_list([], acc) do
acc
end
## drop
@@ -5108,7 +5011,8 @@ end
defimpl Enumerable, for: List do
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(_list), do: {:error, __MODULE__}
@@ -5163,16 +5067,7 @@ defimpl Enumerable, for: Range do
end
# TODO: Remove me on v2.0
reduce =
quote generated: true do
reduce(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(acc),
var!(fun)
)
end
def unquote(reduce) do
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
step = if first <= last, do: 1, else: -1
reduce(Map.put(range, :step, step), acc, fun)
end
@@ -5195,12 +5090,12 @@ defimpl Enumerable, for: Range do
{:done, acc}
end
def member?(first..last//step, value) when is_integer(value) and step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
end
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}
def member?(first..last//step, value) when is_integer(value) do
if step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
else
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
end
# TODO: Remove me on v2.0
@@ -5219,17 +5114,11 @@ defimpl Enumerable, for: Range do
end
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
# TODO: Remove me on v2.0
slice =
quote generated: true do
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
end
def unquote(slice) do
def slice(%{__struct__: Range, first: first, last: last} = range) do
step = if first <= last, do: 1, else: -1
slice(Map.put(range, :step, step))
end
+34 -17
View File
@@ -26,7 +26,7 @@ defmodule Exception do
@typedoc "The exception type"
@type t :: %{
required(:__struct__) => module,
required(:__exception__) => term,
required(:__exception__) => true,
optional(atom) => any
}
@@ -77,7 +77,7 @@ defmodule Exception do
@doc false
@deprecated "Use Kernel.is_exception/1 instead"
def exception?(term)
def exception?(%_{__exception__: _}), do: true
def exception?(%_{__exception__: true}), do: true
def exception?(_), do: false
@doc """
@@ -89,7 +89,7 @@ defmodule Exception do
return a descriptive error message instead.
"""
@spec message(t) :: String.t()
def message(%module{__exception__: _} = exception) do
def message(%module{__exception__: true} = exception) do
try do
module.message(exception)
rescue
@@ -123,7 +123,7 @@ defmodule Exception do
@spec normalize(:error, any, stacktrace) :: t
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
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(_kind, payload, _stacktrace), do: payload
@@ -287,10 +287,10 @@ defmodule Exception do
end
end
defp map_node?({:is_map, _, [_]}), do: true
defp map_node?(_), do: false
defp map_key_node?({:is_map_key, _, [_, _]}), do: true
defp map_key_node?(_), do: false
defp is_map_node?({:is_map, _, [_]}), do: true
defp is_map_node?(_), do: false
defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
defp is_map_key_node?(_), do: false
defp struct_validation_node?(
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
@@ -304,16 +304,16 @@ defmodule Exception do
defp struct_validation_node?(_), do: false
defp struct_macro?(
defp is_struct_macro?(
{:and, _,
[
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [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, _,
@@ -328,12 +328,12 @@ defmodule Exception do
%{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
if struct_macro?(guard) do
if is_struct_macro?(guard) do
undo_is_struct_guard(guard)
else
guard
@@ -1388,7 +1388,6 @@ defmodule CompileError do
* `: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
* `: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.
"""
@@ -1457,6 +1456,20 @@ defmodule BadFunctionError do
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
@moduledoc """
An exception raised when a map is expected, but something else was given.
@@ -1930,8 +1943,8 @@ defmodule FunctionClauseError do
For example:
iex> List.duplicate(:ok, -3)
** (FunctionClauseError) no function clause matching in List.duplicate/2
iex> URI.parse(:wrong_argument)
** (FunctionClauseError) no function clause matching in URI.parse/1
The following fields of this exception are public and can be accessed freely:
@@ -2591,6 +2604,10 @@ defmodule ErlangError do
%BadFunctionError{term: term}
end
def normalize({:badstruct, struct, term}, _stacktrace) do
%BadStructError{struct: struct, term: term}
end
def normalize({:badmatch, term}, _stacktrace) do
%MatchError{term: term}
end
+24 -116
View File
@@ -400,13 +400,6 @@ defmodule File do
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
File.read("hello.txt")
@@ -415,24 +408,15 @@ defmodule File do
File.read("non_existing.txt")
#=> {:error, :enoent}
"""
@spec read(Path.t(), [exists_option]) ::
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
when exists_option: :raw
def read(path, opts \\ []) do
:file.read_file(IO.chardata_to_string(path), opts)
@spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
def read(path) do
:file.read_file(IO.chardata_to_string(path))
end
@doc """
Returns a binary with the contents of the given filename,
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
File.read!("hello.txt")
@@ -441,9 +425,9 @@ defmodule File do
File.read!("non_existing.txt")
** (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
def read!(path, opts \\ []) do
case read(path, opts) do
@spec read!(Path.t()) :: binary
def read!(path) do
case read(path) do
{:ok, binary} ->
binary
@@ -710,7 +694,7 @@ defmodule File do
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
{:error, :enoent}
File.touch("/tmp/a.txt", 1_544_519_753)
File.touch("/tmp/a.txt", 1544519753)
#=> :ok
"""
@@ -749,7 +733,7 @@ defmodule File do
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.touch!("/tmp/a.txt", 1_544_519_753)
File.touch!("/tmp/a.txt", 1544519753)
"""
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
@@ -1103,7 +1087,7 @@ defmodule File do
@doc ~S"""
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
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`
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.
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`
If a file already exists in the destination, it invokes the optional `on_conflict`
callback given as an option. See "Options" for more information.
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`
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
* `: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
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
# Copies file "a.txt" to "b.txt"
@@ -1175,16 +1144,11 @@ defmodule File do
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
File.cp_r("non_existing.txt", "copy.txt")
#=> {:error, :enoent, "non_existing.txt"}
# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
#=> {:error, :enoent}
"""
@spec cp_r(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
dereference_symlinks: boolean()
) ::
{: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
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
dereference? = Keyword.get(options, :dereference_symlinks, false)
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
source =
source
@@ -1218,25 +1181,9 @@ defmodule File do
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
source_parts = source |> Path.expand() |> Path.split()
dest_parts = destination |> Path.expand() |> Path.split()
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
case do_cp_r(source, destination, on_conflict, dereference?, []) do
{:error, _, _} = error -> error
res -> {:ok, res}
end
end
@@ -1257,8 +1204,7 @@ defmodule File do
"""
@spec cp_r!(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
dereference_symlinks: boolean()
) :: [binary]
def cp_r!(source, destination, options \\ []) do
case cp_r(source, destination, options) do
@@ -1275,7 +1221,7 @@ defmodule File do
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
{:ok, :regular} ->
case do_cp_file(src, dest, on_conflict, acc) do
@@ -1288,15 +1234,8 @@ defmodule File do
{:ok, :symlink} ->
case :file.read_link(src) do
{:ok, link} when dereference != nil ->
resolved = Path.expand(link, Path.dirname(src))
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} when dereference? ->
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
{:ok, link} ->
do_cp_link(link, src, dest, on_conflict, acc)
@@ -1310,35 +1249,9 @@ defmodule File do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
files
|> Enum.reduce_while([dest | acc], fn x, 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
Enum.reduce(files, [dest | acc], fn x, acc ->
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
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, dest}
@@ -1349,7 +1262,7 @@ defmodule File do
end
{:ok, _} ->
acc
{:error, :eio, src}
{:error, reason} ->
{:error, reason, src}
@@ -1357,7 +1270,7 @@ defmodule File do
end
# 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
end
@@ -2252,12 +2165,7 @@ defmodule File do
def stream!(path, line_or_bytes, modes)
def stream!(path, modes, line_or_bytes) when is_list(modes) do
# TODO: Remove me on Elixir 2.0
IO.warn(
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
"invoke File.stream!(path, line_or_bytes, modes) instead"
)
# TODO: Deprecate this on Elixir v1.20
stream!(path, line_or_bytes, modes)
end
+6 -13
View File
@@ -119,7 +119,7 @@ defmodule File.Stream do
counter = fn device ->
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
{:ok, open!(stream, modes, counter)}
@@ -229,28 +229,21 @@ defmodule File.Stream do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
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
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) ->
count_lines(device, path, pattern, read, count, last_byte)
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
:eof ->
case last_byte do
:empty -> 0
?\n -> count
_ -> count + 1
end
count
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
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: false}), do: &IO.read(&1, @read_ahead_size)
end
+121 -137
View File
@@ -42,7 +42,7 @@ defmodule Float do
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/)
"""
@@ -186,9 +186,10 @@ defmodule Float do
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
# :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, _, acc) do
# 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
# rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, true = _e?, acc) do
acc
|> add_dot(dot?)
|> :lists.reverse()
@@ -199,6 +200,16 @@ defmodule Float do
float -> {float, rest}
end
defp parse_unsigned(rest, dot?, false = _e?, acc) do
float =
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
{float, rest}
end
defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: [?0, ?. | acc]
@@ -344,12 +355,15 @@ defmodule Float do
"""
@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, 0) when float == 0.0, do: float
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
rounded -> rounded
end
@@ -363,170 +377,140 @@ defmodule Float do
raise ArgumentError, invalid_precision_message(precision)
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(float, precision, mode) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
defp round(float, precision, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
cond do
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump
# to 10^-precision; everything else rounds to signed zero).
exp == 0 ->
tiny_round(sign, precision, mode)
# Precision beyond 15 digits
count >= 104 ->
case rounding do
: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.
exp - 1075 >= 0 ->
# We are asking more precision than we have
count <= precision ->
float
true ->
mantissa = @power_of_2_to_52 ||| mantissa
shift = 1075 - exp
do_round(sign, mantissa, shift, precision, mode)
# Difference in precision between float and asked precision
# We subtract 1 because we need to calculate the remainder too
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
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign.
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do
tiny_round(sign, precision, mode)
# TODO remove once we require Erlang/OTP 27+
# This function tricks the compiler to avoid this bug in previous versions:
# 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
defp do_round(sign, mantissa, shift, precision, mode) do
power = power_of_10(precision)
product = mantissa * power
half = 1 <<< (shift - 1)
quotient = product >>> shift
remainder = product - (quotient <<< shift)
rounded_int = round_step(mode, sign, quotient, remainder, half)
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
end
cond do
rounded_int == 0 ->
signed_zero(sign)
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, last_count, acc)
end
rounded_int < @power_of_2_to_52 <<< 1 ->
# Both rounded_int and power fit in 53 bits, so IEEE float division
# is correctly rounded.
result = rounded_int / power
if sign == 1, do: -result, else: result
defp decompose(<<>>, _count, last_count, acc) do
{acc, last_count}
end
true ->
bignum_to_float(sign, rounded_int, power)
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
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
defp round_step(:half_up, _sign, quotient, remainder, half) do
if remainder >= half, do: quotient + 1, else: quotient
end
defp decimal_to_float(sign, num, den, exp) do
quo = div(num, den)
rem = num - quo * den
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient
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
tmp =
case den >>> 1 do
den when rem > den -> quo + 1
den when rem < den -> quo
_ when (quo &&& 1) === 1 -> quo + 1
_ -> quo
end
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient
# or when rounding carries. Rebalance into the canonical [2^52, 2^53)
# 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
tmp = tmp - @power_of_2_to_52
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
tmp
end
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53)
# and the resulting float = numerator/denominator * 2^(exp-52).
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
new_power = power <<< shift_adjust
defp rounding(:floor, 1, _num, div), do: div + 1
defp rounding(:ceil, 0, _num, div), do: div + 1
if rounded_int < new_power <<< 53,
do: {rounded_int, new_power, 52 + shift_adjust},
else: {rounded_int, new_power <<< 1, 53 + shift_adjust}
end
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}
defp rounding(:half_up, _sign, num, div) do
case rem(num, 10) do
rem when rem < 5 -> div
rem when rem >= 5 -> div + 1
end
end
defp bit_length(0), do: 0
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
defp rounding(_, _, _, div), do: div
Enum.reduce(0..15, 1, fn exponent, acc ->
defp power_of_10(unquote(exponent)), do: unquote(acc)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_10(unquote(x)), do: unquote(acc)
acc * 10
end)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end)
@doc """
Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator.
+7 -48
View File
@@ -210,14 +210,13 @@ defmodule GenServer do
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
child should be restarted, defaults to `:permanent`
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
shut down the child, either immediately or by giving it time to shut down,
defaults to `5_000`
shut down the child, either immediately or by giving it time to shut down
For example:
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
`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
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)
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
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
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)
* [`:gen_server` module documentation](`:gen_server`)
* [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`
after a delay to attempt a restart.
Returning `{:error, reason}` will cause `start_link/3` to return
`{:error, reason}`.
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.
Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}` and the process to exit with reason `reason` without
entering the loop or calling `c:terminate/2`.
"""
@callback init(init_arg :: term) ::
{:ok, state}
@@ -863,7 +822,7 @@ defmodule GenServer do
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@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"
@type options :: [option]
+3 -14
View File
@@ -275,6 +275,8 @@ defprotocol Inspect do
end
defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
@@ -689,11 +691,6 @@ defimpl Inspect, for: Any do
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
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
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
@@ -721,15 +718,7 @@ defimpl Inspect, for: Range do
end
# TODO: Remove me on v2.0
inspect =
quote generated: true do
inspect(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(opts)
)
end
def unquote(inspect) do
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
step = if first <= last, do: 1, else: -1
inspect(Map.put(range, :step, step), opts)
end
+3 -3
View File
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
* `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option.
It accepts a positive integer or `:infinity`. It defaults to `200` since
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
It accepts a positive integer or `:infinity`. It defaults to `100` since
`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`.
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
charlists: :infer,
custom_options: [],
inspect_fun: &Inspect.inspect/2,
limit: 200,
limit: 100,
pretty: false,
printable_limit: 4096,
safe: true,
+5 -83
View File
@@ -18,45 +18,6 @@ defmodule Integer do
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 """
Determines if `integer` is odd.
@@ -211,35 +172,6 @@ defmodule Integer do
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 """
Returns the ordered digits for the given `integer`.
@@ -297,9 +229,8 @@ defmodule Integer do
defp undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _)
when is_integer(digit) and (digit >= base or digit <= -base),
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(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.
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`.
Raises an error if `base` is less than 2 or more than 36.
@@ -329,9 +260,6 @@ defmodule Integer do
iex> Integer.parse("three")
:error
iex> Integer.parse("404 not found")
{404, " not found"}
iex> Integer.parse("34", 10)
{34, ""}
@@ -532,12 +460,8 @@ defmodule Integer do
iex> Integer.extended_gcd(10, 0)
{10, 1, 0}
iex> Integer.extended_gcd(-10, 0)
{10, -1, 0}
iex> Integer.extended_gcd(0, 10)
{10, 0, 1}
iex> Integer.extended_gcd(0, -10)
{10, 0, -1}
iex> Integer.extended_gcd(0, 0)
{0, 0, 0}
@@ -545,10 +469,8 @@ defmodule Integer do
@doc since: "1.12.0"
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
def extended_gcd(0, 0), do: {0, 0, 0}
def extended_gcd(0, b) when b > 0, do: {b, 0, 1}
def extended_gcd(0, b) when b < 0, do: {-b, 0, -1}
def extended_gcd(a, 0) when a > 0, do: {a, 1, 0}
def extended_gcd(a, 0) when a < 0, do: {-a, -1, 0}
def extended_gcd(0, b), do: {b, 0, 1}
def extended_gcd(a, 0), do: {a, 1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0)
-23
View File
@@ -796,29 +796,6 @@ defmodule IO do
:erlang.iolist_size(iodata)
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
def each_stream(device, line_or_codepoints) do
case read(device, line_or_codepoints) do
+17 -22
View File
@@ -8,29 +8,24 @@ defprotocol JSON.Encoder do
If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [...]}
@derive {JSON.Encoder, only: [....]}
defstruct ...
Additionally, you can exclude specific fields using the `:except` option or
encode all fields by omitting both options entirely, but these should be used
with caution:
@derive {JSON.Encoder, except: [...]}
defstruct ...
It is also possible to encode all fields or skip some fields via the
`:except` option:
@derive JSON.Encoder
defstruct ...
> #### Leaking Private Information {: .error}
>
> Prefer using `:only` to avoid accidentally leaking private information when
> new fields are added. Other approaches should be used with auction.
> The `:except` approach should be used carefully to avoid
> 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
to encode to JSON:
Finally, if you don't own the struct you want 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, except: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)
"""
@@ -67,7 +62,7 @@ defprotocol JSON.Encoder do
{io, _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))], ?,}
end)
@@ -130,25 +125,25 @@ end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:json.encode_binary(value)
:elixir_json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:json.encode_list(value, encoder)
:elixir_json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:json.encode_integer(value)
:elixir_json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:json.encode_float(value)
:elixir_json.encode_float(value)
end
end
@@ -408,7 +403,7 @@ defmodule JSON do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:json.decode(binary, acc, Map.new(decoders))
:elixir_json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
@@ -528,16 +523,16 @@ defmodule JSON do
end
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),
do: :json.encode_integer(value)
do: :elixir_json.encode_integer(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),
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__),
do: JSON.Encoder.Map.encode(value, encoder)
+103 -129
View File
@@ -1999,12 +1999,6 @@ defmodule Kernel do
{:case, extra ++ meta, args}
end
defp x_is_false_or_nil do
quote generated: true do
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
end
end
@doc """
Strictly boolean "or" operator.
@@ -2069,20 +2063,15 @@ defmodule Kernel do
end
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(
[optimize_boolean: true, type_check: {:case, operator}],
{:case, [], [check, [do: bools ++ error]]}
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(check) do
false -> unquote(false_clause)
true -> unquote(true_clause)
other -> :erlang.error({:badbool, unquote(operator), other})
end
end
)
end
@@ -2109,10 +2098,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: {:case, :"!!"}],
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> false
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
_ -> true
end
end
@@ -2123,10 +2112,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: {:case, :!}],
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> true
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
_ -> false
end
end
@@ -2752,7 +2741,7 @@ defmodule Kernel do
nil ->
quote do
case unquote(term) do
%_{__exception__: _} -> true
%_{__exception__: true} -> true
_ -> false
end
end
@@ -2764,7 +2753,8 @@ defmodule Kernel do
quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term))
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end
end
end
@@ -2791,7 +2781,7 @@ defmodule Kernel do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: _} -> true
%{__struct__: ^name, __exception__: true} -> true
_ -> false
end
@@ -2809,7 +2799,8 @@ defmodule Kernel do
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term))
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
end
end
end
@@ -3533,7 +3524,8 @@ defmodule Kernel do
end
@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
@@ -3611,7 +3603,7 @@ defmodule Kernel do
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
"""
defmacro match?(pattern, expression) do
defmacro match?(pattern, expr) do
success =
quote do
unquote(pattern) -> true
@@ -3622,7 +3614,7 @@ defmodule Kernel do
_ -> false
end
{:case, [], [expression, [do: success ++ failure]]}
{:case, [], [expr, [do: success ++ failure]]}
end
@doc """
@@ -4052,10 +4044,10 @@ defmodule Kernel do
defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case(
[optimize_boolean: true, type_check: {:case, :if}],
[optimize_boolean: true, type_check: :expr],
quote 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)
end
end
@@ -4103,15 +4095,9 @@ defmodule Kernel do
end
defp build_unless(condition, do: do_clause, else: else_clause) do
annotate_case(
[optimize_boolean: true, type_check: {:case, :unless}],
quote do
case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(do_clause)
_ -> unquote(else_clause)
end
end
)
quote do
if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
end
end
defp build_unless(_condition, _arguments) do
@@ -4379,10 +4365,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
annotate_case(
[type_check: {:case, :&&}],
[type_check: :expr],
quote do
case unquote(left) do
x when unquote(x_is_false_or_nil()) ->
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x
_ ->
@@ -4422,10 +4408,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "||")
annotate_case(
[type_check: {:case, :||}],
[type_check: :expr],
quote do
case unquote(left) do
x when unquote(x_is_false_or_nil()) ->
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)
x ->
@@ -4703,13 +4689,17 @@ defmodule Kernel do
false
[] ->
# inlined as false in erlang pass
quote(do: :lists.member(unquote(left), []))
quote do
_ = unquote(left)
false
end
[head | tail] = list ->
case in_body? do
false -> in_list(left, head, tail, expand, list)
true -> quote(do: :lists.member(unquote(left), unquote(right)))
# We only expand lists in the body if they are relatively
# short and it is made only of literal expressions.
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
%{} = right ->
@@ -4721,7 +4711,7 @@ defmodule Kernel do
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step)))
_ 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)
@@ -4756,6 +4746,12 @@ defmodule Kernel do
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
in_range_literal(left, first, last, step)
end
@@ -4763,8 +4759,8 @@ defmodule Kernel do
defp in_range(left, first, last, step) do
quoted =
quote do
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
unquote(generated_is_integer(last)) and
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
:erlang.is_integer(unquote(last)) and
((:erlang.>(unquote(step), 0) and
unquote(increasing_compare(left, first, last))) or
(:erlang.<(unquote(step), 0) and
@@ -4780,9 +4776,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step > 0 do
quoted =
quote generated: true do
Kernel.and(
unquote(generated_is_integer(left)),
quote do
:erlang.andalso(
:erlang.is_integer(unquote(left)),
unquote(increasing_compare(left, first, last))
)
end
@@ -4792,9 +4788,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step < 0 do
quoted =
quote generated: true do
Kernel.and(
unquote(generated_is_integer(left)),
quote do
:erlang.andalso(
:erlang.is_integer(unquote(left)),
unquote(decreasing_compare(left, first, last))
)
end
@@ -4808,28 +4804,36 @@ defmodule Kernel do
defp in_range_step(quoted, left, first, step) do
quote do
Kernel.and(
:erlang.andalso(
unquote(quoted),
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
)
end
end
defp in_list(left, head, tail, expand, right) do
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail])
:lists.foldl(&quote(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
:lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
end
defp comp(left, {:|, _, [head, tail]}, expand, right) do
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
case expand.(tail) do
[] ->
quote(do: :erlang."=:="(unquote(left), unquote(head)))
[tail_head | tail] ->
quote do
Kernel.or(
:erlang.orelse(
: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
@@ -4838,17 +4842,13 @@ defmodule Kernel do
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)))
end
defp generated_is_integer(arg) do
quote generated: true, do: :erlang.is_integer(unquote(arg))
end
defp increasing_compare(var, first, last) do
quote do
Kernel.and(
:erlang.andalso(
:erlang.>=(unquote(var), unquote(first)),
:erlang."=<"(unquote(var), unquote(last))
)
@@ -4857,7 +4857,7 @@ defmodule Kernel do
defp decreasing_compare(var, first, last) do
quote do
Kernel.and(
:erlang.andalso(
:erlang."=<"(unquote(var), unquote(first)),
:erlang.>=(unquote(var), unquote(last))
)
@@ -5214,11 +5214,6 @@ defmodule Kernel do
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(_), do: []
@@ -5599,8 +5594,8 @@ defmodule Kernel do
## Types
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
used:
is called `t`. To define a struct inside a type, the struct literal syntax
is used:
defmodule User do
defstruct name: "John", age: 25
@@ -5871,15 +5866,11 @@ defmodule Kernel do
end
@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
guards. The module that contains the custom guard must be required before usage.
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).
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5887,30 +5878,31 @@ defmodule Kernel do
## Example
For example, to define a guard similar to `Integer.is_even/1`, you can write:
defmodule Integer.Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end
which can then be used as:
defmodule Collatz do
@moduledoc "Tools for working with the Collatz sequence."
import Integer.Guards
require Integer.Guards
Integer.Guards.is_even(3)
#=> false
@doc "Determines the number of steps `n` takes to reach `1`."
# If this function never converges, please let me know what `n` you used.
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
inside and outside of guards, preserving their respective semantics.
defp step(n, step_count) when is_even(n) do
step(div(n, 2), step_count + 1)
end
When invoked inside a guard, it behaves as if the right-hand side of
`when` is injected as part of the guard, replacing the custom guard
arguments by the expressions given as inputs.
defp step(n, step_count) do
step(3 * n + 1, step_count + 1)
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"
@spec defguard(Macro.t()) :: Macro.t()
@@ -6325,15 +6317,7 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
# The compiling process may override the callback by putting it in
# 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
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end
@@ -6394,7 +6378,7 @@ defmodule Kernel do
With a timeout:
iex> to_timeout(5_400_000)
iex> to_timeout(5400000)
5400000
iex> to_timeout(:infinity)
:infinity
@@ -6422,20 +6406,12 @@ defmodule Kernel do
{microsecond, _precision} = duration.microsecond
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
total =
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
if total < 0 do
raise ArgumentError,
"duration must be positive, got: #{inspect(duration)}"
end
total
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
end
end
@@ -6643,13 +6619,11 @@ defmodule Kernel do
defmacro sigil_r(term, modifiers)
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)
compile_regex(binary, options)
end
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)}
compile_regex(tuple, options)
end
+1 -1
View File
@@ -338,7 +338,7 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | verbose_compile: true})
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})
end
-36
View File
@@ -21,15 +21,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout)
end
@doc """
Invoked during module expansion to annotate a require
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 """
Invoked during module expansion to annotate an alias
must be warned if unused.
@@ -66,11 +57,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_export, module})
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
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
: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)
end
@doc false
def collect_unused_requires(pid) do
:gen_server.call(pid, :unused_requires, @timeout)
end
# Callbacks
def init(:ok) do
state = %{
aliases: %{},
imports: %{},
requires: %{},
references: %{},
exports: %{},
cache: %{},
@@ -170,18 +150,6 @@ defmodule Kernel.LexicalTracker do
{:reply, Enum.sort(imports), state}
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
{compile, runtime} = partition(Map.to_list(state.references), [], [])
{: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)}
end
def handle_cast({:warn_require, module, meta, alias}, state) do
{:noreply, put_in(state.requires[module], {meta, alias})}
end
@doc false
def handle_info(_msg, state) do
{:noreply, state}
+18 -36
View File
@@ -25,8 +25,9 @@ defmodule Kernel.ParallelCompiler do
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
{:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
@@ -34,8 +35,7 @@ defmodule Kernel.ParallelCompiler do
dest: Path.t(),
beam_timestamp: term(),
return_diagnostics: boolean(),
max_concurrency: pos_integer(),
purge_compiler_modules: boolean()
max_concurrency: pos_integer()
]
@typedoc """
@@ -51,8 +51,8 @@ defmodule Kernel.ParallelCompiler do
@doc """
Starts a task for parallel compilation.
"""
# TODO: Remove me on Elixir 2.0
@deprecated "Use `pmap/2` instead"
# TODO: Deprecate this on Elixir v1.20.
@doc deprecated: "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun)
send(task.pid, ref)
@@ -197,11 +197,8 @@ defmodule Kernel.ParallelCompiler do
deprecation warnings, and type checking should run. Defaults to `true`.
We recommend disabling it only for debugging purposes.
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
each module type check, and group pass checker
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
after compilation (see `Code.purge_compiler_modules/0`)
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
this information is only used to properly annotate the BEAM files before
@@ -339,14 +336,7 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(schedulers, checker, files, output, options) do
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = :erlang.send_after(threshold, self(), :threshold_check)
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
timer_ref = Process.send_after(self(), :threshold_check, threshold)
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
@@ -363,8 +353,7 @@ defmodule Kernel.ParallelCompiler do
long_compilation_threshold: threshold,
schedulers: schedulers,
checker: checker,
verification?: Keyword.get(options, :verification, true),
purge_compiler_modules: purge_compiler_modules
verification?: Keyword.get(options, :verification, true)
})
Process.cancel_timer(state.timer_ref)
@@ -459,7 +448,7 @@ defmodule Kernel.ParallelCompiler do
modules = write_module_binaries(result, state.output, state)
profile(state, "after compile callback", state.after_compile)
{runtime_warnings, errors} =
runtime_warnings =
if state.verification? do
profile(
state,
@@ -470,19 +459,11 @@ defmodule Kernel.ParallelCompiler do
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
)
else
{[], []}
[]
end
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
case errors do
[] ->
{{:ok, modules, info}, state}
_ ->
IO.puts(:stderr, "== Type checking failed with errors ==")
{{:error, errors, info}, state}
end
{{:ok, modules, info}, state}
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
@@ -589,11 +570,11 @@ defmodule Kernel.ParallelCompiler do
case cycle_return do
{: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)
{:compile, [], extra_warnings} ->
state.purge_compiler_modules.()
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} ->
@@ -840,7 +821,7 @@ defmodule Kernel.ParallelCompiler do
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}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
@@ -898,7 +879,8 @@ defmodule Kernel.ParallelCompiler do
end
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 =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
+34 -22
View File
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
Let's give it a try on IEx:
iex> opts = %{"width" => 10, "height" => 15}
iex> with {:ok, width} <- Map.fetch(opts, "width"),
...> {:ok, height} <- Map.fetch(opts, "height") do
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{:ok, 150}
@@ -1603,13 +1603,21 @@ defmodule Kernel.SpecialForms do
If all clauses match, the `do` block is executed, returning its result.
Otherwise the chain is aborted and the non-matched value is returned:
iex> opts = %{"width" => 10}
iex> with {:ok, width} <- Map.fetch(opts, "width"),
...> {:ok, height} <- Map.fetch(opts, "height") do
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
: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
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
`with` in the case of a failed match:
with {:ok, content} <- File.read(path),
:ok <- File.write(path, [content, "!"]) do
:ok
else
{:error, reason} ->
Logger.error("could not append ! to \#{path} with reason: \#{reason}")
:error
end
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
The `else` block works like a `case`: it can have multiple clauses,
and the first match will be used. Variables bound inside `with`
(such as `content` in this example) are not available in the `else` block.
and the first match will be used. Variables bound inside `with` (such as
`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`
exception is raised.
@@ -1975,13 +1987,13 @@ defmodule Kernel.SpecialForms do
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:
iex> case 2 do
...> value when value in [1, 2] ->
iex> case :two do
...> value when value in [:one, :two] ->
...> "#{value} has been matched"
...> 3 ->
...> "3 has been matched"
...> :three ->
...> "three has been matched"
...> end
"2 has been matched"
"two has been matched"
"""
defmacro case(condition, clauses), do: error!([condition, clauses])
@@ -2343,7 +2355,7 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args])
@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.
If there is no matching message, the current process waits until a matching
+5 -15
View File
@@ -255,20 +255,9 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = type_pair ->
cond do
# This is a built-in type since OTP 29 but it just generates a warning for now
{name, arity} == {:record, 0} ->
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
if built_in_type?(name, arity) do
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
compile_error(env, message)
end
if Map.has_key?(type_pairs, type_pair) do
@@ -610,7 +599,8 @@ defmodule Kernel.Typespec do
types =
:lists.map(
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,
struct_info
)
+3 -3
View File
@@ -266,7 +266,7 @@ defmodule Kernel.Utils do
module.exception([])
end
def raise(%_{__exception__: _} = exception) do
def raise(%_{__exception__: true} = exception) do
exception
end
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
end
end
end
@@ -369,7 +369,7 @@ defmodule Kernel.Utils do
end
# 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} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
+19 -29
View File
@@ -119,23 +119,14 @@ defmodule Keyword do
iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
[foo: :atom, bar: :atom, baz: :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"
@spec from_keys([key], value) :: t(value)
def from_keys(keys, value) when is_list(keys) do
:lists.map(
fn
key when is_atom(key) -> {key, value}
other -> raise ArgumentError, "expected a list of atoms as keys, got: #{inspect(other)}"
end,
keys
)
:lists.map(&{&1, value}, keys)
end
@doc """
@@ -270,34 +261,34 @@ defmodule Keyword do
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
{:ok, keyword()} | {:error, [atom]}
def validate(keyword, values) when is_list(keyword) and is_list(values) do
validate(keyword, values, [], keyword, [])
validate(keyword, values, [], [], [])
end
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
case find_key!(key, values1, values2) do
{values1, values2} ->
validate(keyword, values1, values2, original, bad_keys)
validate(keyword, values1, values2, [pair | acc], bad_keys)
:error ->
case find_key!(key, values2, values1) do
{values1, values2} ->
validate(keyword, values1, values2, original, bad_keys)
validate(keyword, values1, values2, [pair | acc], bad_keys)
:error ->
validate(keyword, values1, values2, original, [key | bad_keys])
validate(keyword, values1, values2, acc, [key | bad_keys])
end
end
end
defp validate([], values1, values2, original, []) do
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
defp validate([], values1, values2, acc, []) do
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
end
defp validate([], _values1, _values2, _original, bad_keys) do
defp validate([], _values1, _values2, _acc, bad_keys) do
{:error, bad_keys}
end
defp validate([pair | _], _values1, _values2, _original, []) do
defp validate([pair | _], _values1, _values2, _acc, []) do
raise ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
end
@@ -446,7 +437,7 @@ defmodule Keyword do
end
@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`
is not present) and must return a two-element tuple: the current value
@@ -492,7 +483,7 @@ defmodule Keyword do
defp get_and_update([{key, current} | t], acc, key, fun) do
case fun.(current) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
{get, :lists.reverse(acc, [{key, value} | t])}
:pop ->
{current, :lists.reverse(acc, t)}
@@ -518,8 +509,7 @@ defmodule Keyword do
end
@doc """
Gets the value for `key` and updates it in one pass, deleting duplicate keys,
raising if `key` can't be found in `keywords`.
Gets the value under `key` and updates it. Raises if there is no `key`.
The `fun` argument receives the value under `key` and must return a
two-element tuple: the current value (the retrieved value, which can be
@@ -555,21 +545,21 @@ defmodule Keyword do
get_and_update!(keywords, key, fun, [])
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
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
:pop ->
{value, :lists.reverse(acc, t)}
{value, :lists.reverse(acc, keywords)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
defp get_and_update!([{_, _} = h | t], key, fun, acc) do
get_and_update!(t, key, fun, [h | acc])
defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
get_and_update!(keywords, key, fun, [e | acc])
end
defp get_and_update!([], key, _fun, acc) when is_atom(key) do
@@ -965,7 +955,7 @@ defmodule Keyword do
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
true
Comparison between values is done with `===/2`,
Comparison between values is done with `===/3`,
which means integers are not equivalent to floats:
iex> Keyword.equal?([a: 1.0], [a: 1])
+3 -59
View File
@@ -187,10 +187,9 @@ defmodule List do
"""
@spec duplicate(any, 0) :: []
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
defp duplicate(0, _elem, acc), do: acc
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
def duplicate(elem, n) do
:lists.duplicate(n, elem)
end
@doc """
Flattens the given `list` of nested lists.
@@ -298,33 +297,6 @@ defmodule List do
def first([], default), do: default
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
iex> List.first!([])
** (ArgumentError) attempted to get the first element of an empty list
"""
@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 """
Returns the last element in `list` or `default` if `list` is empty.
@@ -353,34 +325,6 @@ defmodule List do
def last([head], _default), do: head
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
iex> List.last!([])
** (ArgumentError) attempted to get the last element of an empty list
"""
@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 """
Receives a list of tuples and returns the first tuple
where the element at `position` in the tuple matches the
+6
View File
@@ -19,6 +19,12 @@ defprotocol List.Chars do
"""
@spec to_charlist(t) :: charlist
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
defimpl List.Chars, for: Atom do
+42 -42
View File
@@ -508,11 +508,13 @@ defmodule Macro do
Generates AST nodes for a given number of required argument
variables using `Macro.unique_var/2`.
The second argument is generally the macro caller's module.
## 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"
@@ -568,11 +570,11 @@ defmodule Macro do
generate another variable, with its own unique counter.
See `var/2` for an alternative.
The second argument is generally the macro caller's module.
## 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"
@@ -971,15 +973,9 @@ defmodule Macro do
}
]
def struct_info!(module, env) when is_atom(module) do
meta = [line: env.line]
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
{:ok, info} ->
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
info
{:error, desc} ->
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
end
@@ -2196,7 +2192,7 @@ defmodule Macro do
Please check `expand_literals/2` for use cases and pitfalls.
"""
@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({:__aliases__, meta, args}, acc, fun) do
@@ -2726,29 +2722,34 @@ defmodule Macro do
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
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
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)})
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
first_entry =
initial_acc =
quote do
unquote(value_var) = unquote(start_ast)
{:multi_value, unquote(escape(start_ast)), unquote(value_var)}
unquote(values_acc_var) = [unquote(value_var)]
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 ->
tag = if i + 1 == len, do: :pipe_end, else: :pipe
[
quote do
unquote(values_ast)
quote do
unquote(value_var) = unquote(step_ast)
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)}
end
end)
[first_entry | pipe_entries]
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
end
]
end
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
@@ -2968,19 +2969,18 @@ defmodule Macro do
result
end
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do
formatted = [
[:faint, "|> ", :reset],
dbg_format_ast_with_value_no_newline(code_ast, value, options)
]
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
result = List.last(values)
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, 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}
end
rest_formatted =
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
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
{[formatted, ?\n], value}
{[first_formatted | rest_formatted], result}
end
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
+3 -4
View File
@@ -300,7 +300,7 @@ defmodule Macro.Env do
iex> Macro.Env.required?(__ENV__, Integer)
false
iex> require Integer, warn: false
iex> require Integer
iex> Macro.Env.required?(__ENV__, Integer)
true
@@ -367,8 +367,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
{:ok, t} | {:error, String.t()}
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -658,7 +657,7 @@ defmodule Macro.Env do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(:elixir_dispatch.stop_generated(args), env)
quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
+54 -105
View File
@@ -287,13 +287,8 @@ defmodule Map do
@doc """
Fetches the value for a specific `key` in the given `map`.
If `map` contains the given `key` then its value is returned
in the shape of `{:ok, value}`. If `map` doesn't contain `key`,
`:error` is returned.
If the type system can verify `:error` is always returned
(which means key is never available in the map), it will emit
an error.
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
If `map` doesn't contain `key`, `:error` is returned.
Inlined by the compiler.
@@ -301,7 +296,7 @@ defmodule Map do
iex> Map.fetch(%{a: 1}, :a)
{:ok, 1}
iex> Map.fetch(%{"foo" => "bar"}, "unknown")
iex> Map.fetch(%{a: 1}, :b)
:error
"""
@@ -312,11 +307,8 @@ defmodule Map do
Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`.
The exclamation mark (`!`) implies this function can raise a `KeyError`
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 below.
If `map` contains `key`, the corresponding value is returned. If
`map` doesn't contain `key`, a `KeyError` exception is raised.
Inlined by the compiler.
@@ -325,54 +317,11 @@ defmodule Map do
iex> Map.fetch!(%{a: 1}, :a)
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
def fetch!(map, key), do: :maps.get(key, map)
def fetch!(map, key) do
:maps.get(key, map)
end
@doc """
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)
%{a: 3, b: 2}
iex> Map.replace(%{"a" => 1}, "b", 2)
%{"a" => 1}
iex> Map.replace(%{a: 1}, :b, 2)
%{a: 1}
"""
@doc since: "1.11.0"
@@ -430,11 +379,7 @@ defmodule Map do
@doc """
Puts a value under `key` only if the `key` already exists in `map`.
The exclamation mark (`!`) implies this function can raise a `KeyError`
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.
If `key` is not present in `map`, a `KeyError` exception is raised.
Inlined by the compiler.
@@ -443,8 +388,8 @@ defmodule Map do
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar")
** (KeyError) key "unknown" not found in:
iex> Map.replace!(%{a: 1}, :b, 2)
** (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)
%{a: 4, b: 2}
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end)
%{"a" => 1, "b" => 2}
iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
%{a: 1, b: 2}
"""
@doc since: "1.14.0"
@@ -547,8 +492,6 @@ defmodule Map do
:erlang.error({:badmap, non_map})
end
defp take([], _map, []), do: %{}
defp take([], _map, acc) do
:maps.from_list(acc)
end
@@ -573,13 +516,15 @@ defmodule Map do
## Examples
iex> Map.get(%{"a" => 1}, "a")
1
iex> Map.get(%{"a" => 1}, "b")
iex> Map.get(%{}, :a)
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
iex> Map.get(%{"a" => nil}, "a", 1)
iex> Map.get(%{a: nil}, :a, 1)
nil
"""
@@ -608,11 +553,15 @@ defmodule Map do
## 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
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
:expensive_value
iex> Map.get_lazy(map, :b, fun)
13
"""
@spec get_lazy(map, key, (-> value)) :: value
@@ -751,10 +700,10 @@ defmodule Map do
iex> Map.pop(%{a: 1}, :a)
{1, %{}}
iex> Map.pop(%{"a" => 1}, "b")
{nil, %{"a" => 1}}
iex> Map.pop(%{"a" => 1}, "b", 3)
{3, %{"a" => 1}}
iex> Map.pop(%{a: 1}, :b)
{nil, %{a: 1}}
iex> Map.pop(%{a: 1}, :b, 3)
{3, %{a: 1}}
"""
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
@@ -777,8 +726,8 @@ defmodule Map do
{1, %{}}
iex> Map.pop!(%{a: 1, b: 2}, :a)
{1, %{b: 2}}
iex> Map.pop!(%{"a" => 1}, "b")
** (KeyError) key "b" not found in:
iex> Map.pop!(%{a: 1}, :b)
** (KeyError) key :b not found in:
...
"""
@@ -804,11 +753,15 @@ defmodule Map do
## 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, %{}}
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
{:expensive_value, %{"a" => 1}}
iex> Map.pop_lazy(map, :b, fun)
{13, %{a: 1}}
"""
@spec pop_lazy(map, key, (-> value)) :: {value, map}
@@ -960,8 +913,8 @@ defmodule Map do
iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
%{a: 2}
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2))
** (KeyError) key "b" not found in:
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
** (KeyError) key :b not found in:
...
"""
@@ -1080,7 +1033,6 @@ defmodule Map do
#=> %{name: "john"}
"""
# TODO: implement this using row polymorphism
@spec from_struct(atom | struct) :: map
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")
@@ -1109,7 +1061,7 @@ defmodule Map do
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
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:
iex> Map.equal?(%{a: 1.0}, %{a: 1})
@@ -1117,7 +1069,11 @@ defmodule Map do
"""
@spec equal?(map, map) :: boolean
def equal?(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
@deprecated "Use Kernel.map_size/1 instead"
@@ -1138,9 +1094,9 @@ defmodule Map do
> #### 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`.
> and `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a map at the end using `Map.new/1`.
## Examples
@@ -1172,13 +1128,6 @@ defmodule Map do
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
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
+1 -5
View File
@@ -55,11 +55,7 @@ defmodule MapSet do
@type value :: term
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
# either via module attributes or by the compiler.
# Defaulting to a broad `term()` type to prevent opaqueness violations.
@typep internal(_value) :: term()
@opaque internal(value) :: :sets.set(value)
@type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term)
+36 -46
View File
@@ -656,8 +656,7 @@ defmodule Module do
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:nifs) :: module()
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
@@ -680,7 +679,7 @@ defmodule Module do
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 create_opts :: [
@@ -959,7 +958,7 @@ defmodule Module do
It handles binaries and atoms.
> #### Untracked compile-time dependencies {: .warning}
> #### Untracked compile-time dependencies {. :warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -985,7 +984,7 @@ defmodule Module do
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
> #### Untracked compile-time dependencies {: .warning}
> #### Untracked compile-time dependencies {. :warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1016,7 +1015,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms.
> #### Untracked compile-time dependencies {: .warning}
> #### Untracked compile-time dependencies {. :warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1040,7 +1039,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms.
> #### Untracked compile-time dependencies {: .warning}
> #### Untracked compile-time dependencies {. :warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1250,18 +1249,17 @@ defmodule Module do
"""
@spec defines?(module, definition) :: boolean
def defines?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
def defines?(module, {name, arity} = tuple)
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
:ets.member(set, {:def, definition})
:ets.member(set, {:def, tuple})
end
@doc """
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.
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
@@ -1277,13 +1275,12 @@ defmodule Module do
"""
@spec defines?(module, definition, def_kind) :: boolean
def defines?(module, {function_name, arity} = definition, def_kind)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 and
def defines?(module, {name, arity} = tuple, def_kind)
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
def_kind in [:def, :defp, :defmacro, :defmacrop] do
{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
_ -> false
end
@@ -1296,9 +1293,7 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec defines_type?(module, definition) :: boolean
def defines_type?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
def defines_type?(module, definition) when is_atom(module) do
Kernel.Typespec.defines_type?(module, definition)
end
@@ -1311,9 +1306,7 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec spec_to_callback(module, definition) :: boolean
def spec_to_callback(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
def spec_to_callback(module, definition) do
Kernel.Typespec.spec_to_callback(module, definition)
end
@@ -1346,7 +1339,7 @@ defmodule Module do
@doc """
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.
This function can only be used on modules that have not yet been compiled.
@@ -1365,7 +1358,7 @@ defmodule Module do
"""
@doc since: "1.13.0"
@spec overridables_in(module) :: [definition]
@spec overridables_in(module) :: [atom]
def overridables_in(module) when is_atom(module) do
assert_not_compiled!(__ENV__.function, module, :all)
:elixir_overridable.overridables_for(module)
@@ -1414,10 +1407,10 @@ defmodule Module do
"""
@spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, def_kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
def definitions_in(module, kind)
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
{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
@doc """
@@ -1448,17 +1441,16 @@ defmodule Module do
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil
@doc since: "1.12.0"
def get_definition(module, {function_name, arity} = _definition, options \\ [])
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 and is_list(options) do
def get_definition(module, {name, arity}, options \\ [])
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
{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, _, _, _}] ->
clauses =
if options[:skip_clauses],
do: [],
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
{:v1, kind, meta, clauses}
@@ -1475,11 +1467,10 @@ defmodule Module do
"""
@doc since: "1.12.0"
@spec delete_definition(module, definition) :: boolean()
def delete_definition(module, {function_name, arity} = _definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
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
@doc """
@@ -1495,20 +1486,20 @@ defmodule Module do
given.
"""
@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)
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 ->
case :elixir_def.take_definition(module, definition) do
case :elixir_def.take_definition(module, tuple) do
false ->
raise ArgumentError,
"cannot make function #{function_name}/#{arity} " <>
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, definition, clause)
:elixir_overridable.record_overridable(module, tuple, clause)
end
other ->
@@ -1517,7 +1508,7 @@ defmodule Module do
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
end
:lists.foreach(func, definitions)
:lists.foreach(func, tuples)
end
@spec make_overridable(module, module) :: :ok
@@ -1575,10 +1566,9 @@ defmodule Module do
exists or one is pending.
"""
@spec overridable?(module, definition) :: boolean
def overridable?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
:elixir_overridable.overridable_for(module, definition) != :not_overridable
def overridable?(module, {function_name, arity} = tuple)
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 do
:elixir_overridable.overridable_for(module, tuple) != :not_overridable
end
@doc """
+4 -28
View File
@@ -181,16 +181,8 @@ defmodule Module.Behaviour do
behaviour not in behaviours ->
{: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 ->
{:error, {:callback_not_defined, behaviour, callbacks}}
{:error, {:behaviour_not_defined, behaviour, callbacks}}
end
end
@@ -223,18 +215,6 @@ defmodule Module.Behaviour do
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()
|> List.flatten()
|> Enum.any?(fn
{_kind, ^behaviour, _optional?} -> true
{_kind, _behaviour, _optional?} -> false
end)
end
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
when map_size(callbacks) == 0 do
context
@@ -410,17 +390,13 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do
behaviour_string = inspect(behaviour)
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
[
"got \"@impl ",
behaviour_string,
inspect(behaviour),
"\" for ",
format_definition(kind, callback),
" but ",
behaviour_string,
" does not specify such callback",
" but this behaviour does not specify such callback",
known_callbacks(callbacks)
]
end
+108 -163
View File
@@ -10,7 +10,6 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type error() :: term()
@type mode() :: :erlang | :elixir | :protocol
@typedoc """
@@ -20,7 +19,6 @@ defmodule Module.ParallelChecker do
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {:profile, :time}
| {atom(), term()}
]
@@ -68,37 +66,17 @@ defmodule Module.ParallelChecker 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,
# we discard their module map on purpose and start from file.
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?)
else
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures)
ref = make_ref()
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
info =
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
List.to_string(beam_location)
else
cache_from_module_map(table, module_map, signatures)
end
end)
inner_spawn(pid, checker, table, module, info, log?)
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()
spawned =
@@ -109,28 +87,48 @@ defmodule Module.ParallelChecker do
{^ref, :cache} ->
Process.link(pid)
location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
{mode, module_tuple} =
cond do
is_binary(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
with {:ok, binary} <- File.read(location),
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <-
: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(exck) do
mode = cache_chunk(table, module, contents)
module_tuple = module_map_to_module_tuple(module_map)
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
:ets.insert(table, {module, mode})
send(checker, {__MODULE__, :cached, module, spawned, ref})
else
_ ->
# Nothing to check, so we notify everyone we are done
:ets.insert(table, {module, :uncached})
send(checker, {__MODULE__, :cached, module, nil, ref})
send(pid, {__MODULE__, module, [], []})
# We only make the module available now, so they are not visible during inference
:ets.insert(table, {module, mode})
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
else
[]
end
send(pid, {__MODULE__, module, warnings})
send(checker, {__MODULE__, :done, module})
end
{:DOWN, ^mon_ref, _, _, _} ->
@@ -138,7 +136,7 @@ defmodule Module.ParallelChecker do
end
end)
register_cache(checker, module, spawned, ref)
register(checker, module, spawned, ref)
:ok
end
@@ -186,47 +184,50 @@ defmodule Module.ParallelChecker do
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
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
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
count = :gen_server.call(checker, :start, :infinity)
{warnings, errors} = collect_results(count, [], [])
diagnostics = collect_results(count, [])
case :erlang.get(:elixir_code_diagnostics) do
:undefined -> :ok
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
end
{warnings, errors}
diagnostics
end
defp collect_results(0, warnings, errors) do
{warnings, errors}
defp collect_results(0, diagnostics) do
diagnostics
end
defp collect_results(count, warnings, errors) do
defp collect_results(count, diagnostics) do
receive do
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
diagnostic = %{diagnostic | file: file && Path.absname(file)}
collect_results(count, [diagnostic | diagnostics])
if Map.get(diagnostic, :severity, :warning) == :error do
collect_results(count, warnings, [diagnostic | errors])
else
collect_results(count, [diagnostic | warnings], errors)
end
{__MODULE__, _module, new_warnings, new_errors} ->
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
{__MODULE__, _module, new_diagnostics} ->
collect_results(count - 1, new_diagnostics ++ diagnostics)
end
end
@doc """
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
end
@doc """
Returns the export kind and deprecation reason for the given MFA from
the cache. If the module does not exist return `:badmodule`,
@@ -263,7 +264,7 @@ defmodule Module.ParallelChecker do
## 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_tuple
@@ -277,23 +278,18 @@ defmodule Module.ParallelChecker do
definitions
)
type_warnings =
profile(module, profile, fn ->
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
type_warnings
diagnostics =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
|> Enum.split_with(&(&1.severity == :warning))
|> group_warnings()
|> emit_warnings(file, log?)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
{warnings, errors}
diagnostics
end
defp module_map_to_module_tuple(module_map) do
@@ -344,16 +340,16 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_diagnostics(triplets) do
defp group_warnings(warnings) do
{ungrouped, grouped} =
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(term)
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, term, {locations, diagnostic}, fn
Map.update(grouped, warning, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
@@ -371,7 +367,7 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped)
end
defp emit_diagnostics(warnings, file, log?) do
defp emit_warnings(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics)
@@ -533,12 +529,8 @@ defmodule Module.ParallelChecker do
:gen_server.call(server, {:unlock, module, mode}, :infinity)
end
defp register_cache_and_checker(server, mode, module, pid, ref) do
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref})
end
defp register_cache(server, module, pid, ref) do
:gen_server.cast(server, {:register_cache, module, pid, ref})
defp register(server, module, pid, ref) do
:gen_server.cast(server, {:register, module, pid, ref})
end
## Server callbacks
@@ -581,13 +573,11 @@ defmodule Module.ParallelChecker do
state = %{
waiting: %{},
caches: [],
checkers: [],
modules: [],
spawned: %{},
schedulers: schedulers,
threshold: threshold,
callback: callback,
profile: Keyword.get(options, :profile),
protocols: [],
table: table
}
@@ -595,9 +585,20 @@ defmodule Module.ParallelChecker do
:gen_server.enter_loop(__MODULE__, [], state)
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}))
{: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
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
@@ -627,21 +628,10 @@ defmodule Module.ParallelChecker do
{:noreply, state}
end
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
{_nil, spawned} = Map.pop(state.spawned, ref)
state =
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)
def handle_info({__MODULE__, :done, module}, state) do
# Unfortunately we cannot assume uniqueness because the same module
# may be defined by mistake several times
{timer, spawned} = Map.pop(state.spawned, module)
timer && Process.cancel_timer(timer)
{:noreply, run_checkers(%{state | spawned: spawned})}
end
@@ -650,41 +640,11 @@ defmodule Module.ParallelChecker do
{:stop, :normal, state}
end
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
end
def handle_cast(
{: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
defp run_checkers(%{modules: []} = state) do
state
end
@@ -693,25 +653,10 @@ defmodule Module.ParallelChecker do
state
end
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
send(pid, {ref, :check, state.profile})
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
spawned = Map.put(state.spawned, ref, timer)
run_checkers(%{state | checkers: checkers, 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.()
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check})
timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | modules: modules, spawned: spawned})
end
end
+85 -297
View File
@@ -4,7 +4,7 @@
defmodule Module.Types do
@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
# there are gradual arguments. Non-gradual arguments always
@@ -24,17 +24,20 @@ defmodule Module.Types do
#
# * :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).
# 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
# feedback).
@modes [:static, :dynamic, :infer]
@modes [:static, :dynamic, :infer, :traversal]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [behaviour_info: 1]
@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,
# those will be replaced anyway. There is also nothing to
# 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)
# In case there are loops, the other we traverse matters,
# so we sort the definitions for determinism
{types, private, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
reduce: {[], [], context()} do
{types, private, context} when kind in [:def, :defmacro] ->
# Optimized version of finder, since we already have the definition
{types, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- defs,
kind in [:def, :defmacro],
reduce: {[], context()} do
{types, context} ->
# Optimized version of finder, since we already the definition
finder = fn _ ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end
@@ -86,13 +88,10 @@ defmodule Module.Types do
{_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
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
{[{fun_arity, inferred} | types], context}
else
{types, private, context}
{types, context}
end
{types, private, context} ->
{types, [def | private], context}
end
# 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} =
Enum.reduce(private, {[], context}, fn
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
warn_unused_def(info, used_sigs, env)
# Find anything undefined within unused functions
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
@@ -126,7 +125,7 @@ defmodule Module.Types do
end
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
defp protocol?(attrs) do
@@ -136,8 +135,8 @@ defmodule Module.Types do
defp impl_for(attrs) do
case List.keyfind(attrs, :__impl__, 0) do
{:__impl__, [protocol: protocol, for: for]} ->
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
{for, protocol.__protocol__(:functions)}
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
{for, protocol.behaviour_info(:callbacks)}
else
nil
end
@@ -155,7 +154,7 @@ defmodule Module.Types do
| List.duplicate(Descr.dynamic(), arity - 1)
]
{_fun_arity, kind, meta, clauses} = def
{fun_arity, kind, meta, clauses} = def
clauses =
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)
end
defp warn_unused_def(fun_arity, kind, meta, used, env) do
default = Keyword.get(meta, :defaults, 0)
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
:ok
end
cond do
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true ->
:ok
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
default == 0 ->
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
:ok
end
default > 0 ->
{name, arity} = fun_arity
min = arity - default
max = arity
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
end
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
end
:ok
@@ -251,27 +249,17 @@ defmodule Module.Types do
context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
if pending != [] do
{used_indexes, unused_indexes} =
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
{used_indexes, unused_indexes} ->
if type_index in pending and not skip_unused_clause?(info, type_index) do
{used_indexes, [clause_index | unused_indexes]}
else
{[clause_index | used_indexes], unused_indexes}
end
end)
clauses_indexes =
for type_index <- pending,
not skip_unused_clause?(info, type_index),
{clause_index, ^type_index} <- mapping,
do: clause_index
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
else
context
end
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end
end
@@ -299,11 +287,11 @@ defmodule Module.Types do
local_sigs ->
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))
{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 =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -316,124 +304,36 @@ defmodule Module.Types do
end
end
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, 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
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
{fun, _arity} = fun_arity
stack = fresh_stack(stack, mode, fun_arity)
base_info = {:def, kind, fun, expected}
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
case clauses do
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, 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.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
{_, _, _, domain, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
{_, _, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, [], [], context}, fn
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
try do
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
{trees, context} =
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, 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
expected
else
Pattern.of_domain(trees, expected, context)
end
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
domain =
case domain do
[] ->
args_types
_ ->
head_args_types = Pattern.of_domain(trees, stack, head_context)
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
end
if type_index == -1 do
mapping = [{index, total} | mapping]
{index + 1, total + 1, previous, domain, mapping, inferred, context}
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
else
mapping = [{index, type_index} | mapping]
{index + 1, total, previous, domain, mapping, inferred, context}
{index + 1, total, [{index, type_index} | mapping], inferred, context}
end
rescue
e ->
@@ -443,69 +343,19 @@ defmodule Module.Types do
domain =
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
inferred = {:infer, domain, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(clauses_context, context)}
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:
#
# 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.union(Descr.upper_bound(no_prev_arg), d)
else
Descr.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
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
@@ -517,58 +367,6 @@ defmodule Module.Types do
defp add_inferred([], args, return, -1, 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.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
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
@@ -631,8 +429,9 @@ defmodule Module.Types do
mode: mode,
# The function for handling local calls
local_handler: handler,
# Reverse arrow handling (nil | :cache | :use)
reverse_arrow: nil
# Control if variable refinement is enabled.
# It is disabled only on dynamic dispatches.
refine_vars: false
}
end
@@ -643,45 +442,34 @@ defmodule Module.Types do
warnings: [],
# All vars and their types
vars: %{},
# Stores special metadata used by list heads and domain keys in patterns
subpatterns: %{},
# Variables that are specific to the current environment/conditional
conditional_vars: nil,
# Track metadata specific to patterns and guards
# Variables and arguments from patterns
pattern_info: nil,
# If type checking has found an error/failure
failed: false,
# Local signatures used by local handler
local_sigs: %{},
# Track which clauses have been used across private local calls
local_used: %{},
# Cached reverse arrows
reverse_arrows: %{}
local_used: %{}
}
end
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
defp fresh_context(context) do
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
%{context | vars: %{}, failed: false}
end
defp restore_context(later_context, %{
vars: vars,
failed: failed,
reverse_arrows: reverse_arrows
}) do
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
defp restore_context(later_context, %{vars: vars, failed: failed}) do
%{later_context | vars: vars, failed: failed}
end
## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message:
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
message: "this clause of #{kind} #{fun}/#{arity} is never used"
}
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
+39 -140
View File
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
@doc """
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 """
Guard function to check if an AST node is a variable.
@@ -143,13 +143,12 @@ defmodule Module.Types.Helpers do
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
Macro.prewalk(expr, %{}, fn node, versions ->
with {var_name, meta, var_context} when is_atom(var_name) and is_atom(var_context) <- node,
false <- String.starts_with?(Atom.to_string(var_name), "_") do
Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
version = meta[:version]
case vars do
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
@@ -161,9 +160,9 @@ defmodule Module.Types.Helpers do
_ ->
{:ok, versions}
end
else
_ -> {node, versions}
end
node, versions ->
{node, versions}
end)
versions
@@ -262,8 +261,7 @@ defmodule Module.Types.Helpers do
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
We also undo some macro expressions done by the Kernel module
and collapse complex expressions.
We also undo some macro expressions done by the Kernel module.
## Options
@@ -329,13 +327,6 @@ defmodule Module.Types.Helpers do
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]} ->
{:to_string, meta, [arg]}
@@ -361,134 +352,42 @@ defmodule Module.Types.Helpers do
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
{:case, meta, [expr, [do: clauses]]} ->
case meta[:type_check] do
{:case, op} ->
case clauses do
[
{:->, _,
{:case, meta, [expr, [do: clauses]]} = case ->
if meta[:type_check] == :expr do
case clauses do
[
{:->, _,
[
[
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
true
]},
{:->, _, [[{:_, _, Kernel}], false]}
]
when op == :! ->
{:!, meta, [expr]}
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
else_block
]},
{:->, _, [[{:_, _, Kernel}], do_block]}
] ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _,
[
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
]
when op == :|| ->
{:||, meta, [expr, right_side]}
[
{:->, _, [[false], else_block]},
{:->, _, [[true], do_block]}
] ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _,
[
[
{:when, _,
[
{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: {:..., [], []}]]}
_ ->
case
end
else
case
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) ->
if is_integer(meta[:capture]) do
{:&, meta, [meta[:capture]]}
+211 -450
View File
@@ -1,6 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
@@ -12,10 +11,10 @@ defmodule Module.Types.Of do
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer()
@float float()
@binary binary()
@bitstring bitstring()
## Variables
@@ -30,53 +29,19 @@ defmodule Module.Types.Of do
@doc """
Marks a variable with error.
This purposedly deletes all traces of the variable,
as it is often invoked when the cause for error is elsewhere.
"""
def error_var({_, meta, _}, 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
def error_var(var, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
case context.vars do
%{^version => _} ->
context
data = %{
type: error_type(),
name: var_name,
context: var_context,
off_traces: []
}
vars ->
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)
put_in(context.vars[version], data)
end
@doc """
@@ -86,70 +51,26 @@ defmodule Module.Types.Of do
or if we are doing a guard analysis or occurrence typing.
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
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_body_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
version = Keyword.fetch!(meta, :version)
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
context =
case context.conditional_vars do
%{} = conditional_vars ->
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
if gradual?(old_type) and type not in [term(), dynamic()] do
case 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)
}
nil ->
context
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
case context do
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context}
%{pattern_info: %{guard_context: guard_context}} ->
new_type = intersection(old_type, type)
case empty?(new_type) do
true when guard_context == :orelse ->
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
else
{old_type, context}
end
end
@@ -160,16 +81,11 @@ defmodule Module.Types.Of do
because we want to refine types. Otherwise we should
use compatibility.
"""
def refine_head_var({_, meta, _}, type, expr, stack, context) do
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_head_var(var, type, expr, stack, context) do
{var_name, meta, var_context} = var
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
%{^version => %{errored: true}} ->
{:ok, error_type(), context}
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
new_type = intersection(type, old_type)
@@ -179,14 +95,26 @@ defmodule Module.Types.Of do
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
data = Map.put(%{data | type: error_type()}, :errored, true)
context = %{context | vars: %{vars | version => data}}
{:error, old_type, context}
{:error, error_type(),
error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
else
context = %{context | vars: %{vars | version => data}}
{:ok, new_type, context}
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
@@ -196,71 +124,11 @@ defmodule Module.Types.Of do
defp new_trace(expr, type, stack, 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
union(acc, type)
end)
{_, context} = refine_body_var(version, type, expr, stack, context)
context
else
context
end
end)
end
## Implementations
impls = [
{Atom, atom()},
{BitString, bitstring()},
{BitString, binary()},
{Float, float()},
{Function, fun()},
{Integer, integer()},
@@ -304,7 +172,7 @@ defmodule Module.Types.Of do
Handles fetching a map key.
"""
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} ->
{value_type, context}
@@ -319,134 +187,107 @@ defmodule Module.Types.Of do
def closed_map(pairs, expected, stack, context, of_fun) do
{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} | single], multiple}
_ ->
{dynamic?, domain, single, [{pos, value_type} | multiple]}
end
end
end)
non_multiple = Enum.reverse(single, domain)
map =
case Enum.reverse(multiple) do
[] ->
closed_map(non_multiple)
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
closed_map(non_multiple ++ [{key, type} | t])
end
|> Enum.reduce(&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} -> {key, union(old_type, new_type)} end)
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
{single, multiple}
end
defp union_negated(negated, new_type, single, multiple) do
{single, matched} =
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
if key in negated do
{{key, old_type}, [key | matched]}
else
{{key, union(old_type, new_type)}, matched}
end
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
# TODO: Use the fallback type to actually indicate if open or closed.
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
end)
multiple =
Enum.map(multiple, fn {keys, old_type} ->
{keys, union(old_type, new_type)}
end)
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
{map, context}
end
defp pairs(pairs, expected, stack, context, of_fun) do
@doc """
Computes the types of key-value pairs.
"""
def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
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 =
with {[key], [], []} <- pos_neg_domain,
{_, expected_value_type} <- map_fetch_key(expected, key) do
with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
expected_value_type
else
_ -> term()
end
{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
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{{[key], [], []}, false, context}
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{false, [key], context}
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)
domain_keys = to_domain_keys(key_type)
pos_neg_domain =
case atom_fetch(key_type) do
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
{:infinite, list} -> {[], list, domain_keys}
:error -> {[], [], domain_keys}
case atom_fetch(key_type) do
{:finite, list} -> {gradual?(key_type), list, context}
_ -> {gradual?(key_type), :none, context}
end
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
{pos_neg_domain, gradual?(key_type), context}
if dynamic?, do: dynamic(map), else: map
end
defp cartesian_map(lists) do
@@ -461,59 +302,50 @@ defmodule Module.Types.Of do
@doc """
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
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
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
{_info, context} = struct_info(struct, meta, stack, context)
if is_nil(info) do
{dynamic(), context}
else
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
with true <- mode != :traversal,
{_, expected_value_type} <- map_fetch(expected, key) do
expected_value_type
else
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
@doc """
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
%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}
{:error, _desc} -> {nil, context}
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
_ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
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
{_, 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
{info, context}
end
end
@@ -530,74 +362,45 @@ defmodule Module.Types.Of do
closed_map(pairs)
end
@doc """
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
## Binary
@doc """
Handles bitstrings.
Handles binaries.
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def bitstring([], _kind, _stack, context) do
{binary(), context}
def binary([], _kind, _stack, context) do
context
end
def bitstring([head], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
{alignment_to_type(alignment), context}
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
end
def bitstring([head | tail], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context)
bitstring_tail(tail, alignment, kind, stack, context)
def binary([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
end
defp bitstring_tail([last], alignment, kind, stack, context) do
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context)
{alignment_to_type(alignment(seg_alignment, alignment)), context}
defp binary_many([last], kind, stack, context) do
binary_segment(last, kind, [@prefix, last], stack, context)
end
defp bitstring_tail([head | tail], alignment, kind, stack, context) do
{seg_alignment, context} =
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
defp binary_many([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
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,
# so we just check the size.
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context)
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
when is_binary(left) or is_number(left) do
{_type, alignment_type} = specifier_type(kind, right)
{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
context
end
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
{type, alignment_type} = specifier_type(kind, right)
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
expr = {:<<>>, meta, args}
{actual, context} =
@@ -614,26 +417,10 @@ defmodule Module.Types.Of do
end
if compatible?(actual, type) do
{alignment_value, context} = specifier_size(kind, right, stack, {:default, 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
specifier_size(kind, right, stack, context)
else
error = {:badbinary, kind, meta, expr, type, actual, context}
{:unknown, error(error, meta, stack, context)}
error(error, meta, stack, context)
end
end
@@ -649,48 +436,39 @@ defmodule Module.Types.Of do
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned}
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned}
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer}
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring}
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring}
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned}
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned}
defp specifier_type(_kind, _specifier), do: {@integer, :integer}
defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
end
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context})
when 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
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{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
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)
{:unknown, compatible_size(actual, expr, stack, context)}
compatible_size(actual, expr, stack, context)
end
# We currently assume the unit always comes before size
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do
{unit, context}
end
defp specifier_size(_kind, _specifier, _stack, align_context) do
align_context
defp specifier_size(_kind, _specifier, _stack, context) do
context
end
defp compatible_size(actual, expr, stack, context) do
@@ -711,12 +489,9 @@ defmodule Module.Types.Of do
"""
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{:finite, mods} ->
{_, mods} ->
{mods, context}
{:infinite, _} ->
{[], context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)}
@@ -729,6 +504,23 @@ defmodule Module.Types.Of do
error(__MODULE__, warning, meta, stack, context)
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
type = if kind == :match, do: "matching", else: "construction"
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
@@ -848,37 +640,6 @@ defmodule Module.Types.Of do
}
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
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
File diff suppressed because it is too large Load Diff
-210
View File
@@ -1,210 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Module.Types.Traverse do
@moduledoc false
# Traverses expressions to find local calls when inference is disabled.
# Literals
def of_expr(literal, _stack, context)
when is_atom(literal) or is_integer(literal) or is_float(literal) or is_binary(literal) or
is_pid(literal) or literal == [] do
context
end
# [expr, ...]
def of_expr(list, stack, context) when is_list(list) do
Enum.reduce(list, context, &of_expr(&1, stack, &2))
end
# {left, right}
def of_expr({left, right}, stack, context) do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
# <<...>>
def of_expr({:<<>>, _meta, args}, stack, context) do
Enum.reduce(args, context, fn
{:"::", _meta, [left, _right]}, context ->
of_expr(left, stack, context)
expr, context ->
of_expr(expr, stack, context)
end)
end
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]}, stack, context) do
context = of_expr(map, stack, context)
context = of_expr(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
def of_expr({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) do
context = of_expr(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
# Map update, tail operator
def of_expr({:|, _meta, [left, right]}, stack, context) do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
# Tuples, maps
def of_expr({container, _meta, exprs}, stack, context) when container in [:{}, :%{}] do
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
end
# left = right, left <- right
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:=, :<-] do
context = of_pattern(left, stack, context)
of_expr(right, stack, context)
end
# Blocks
def of_expr({:__block__, _, args}, stack, context) do
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# cond do ... end
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
Enum.reduce(clauses, context, fn {:->, _meta, [[head], body]}, context ->
context = of_expr(head, stack, context)
of_expr(body, stack, context)
end)
end
# All non-handled -> are patterns
def of_expr({:->, _, [head, body]}, stack, context) do
context = of_pattern(head, stack, context)
of_expr(body, stack, context)
end
# case expr do ... end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
context = of_expr(case_expr, stack, context)
of_expr(clauses, stack, context)
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses}, stack, context) do
of_expr(clauses, stack, context)
end
# try do ... end
def of_expr({:try, _meta, [blocks]}, stack, context) do
Enum.reduce(blocks, context, fn {_, clauses_or_body}, context ->
of_expr(clauses_or_body, stack, context)
end)
end
# receive do ... end
def of_expr({:receive, _meta, [blocks]}, stack, context) do
Enum.reduce(blocks, context, fn
{:do, clauses_or_empty_body}, context ->
of_expr(clauses_or_empty_body, stack, context)
{:after, [{:->, _meta, [[timeout], body]}]}, context ->
context = of_expr(timeout, stack, context)
of_expr(body, stack, context)
end)
end
# for, with
def of_expr({op, _meta, [_ | _] = args}, stack, context) when op in [:for, :with] do
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# fun.(args)
def of_expr({{:., _meta, [fun]}, _call_meta, args}, stack, context) do
context = of_expr(fun, stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# remote.fun(args)
def of_expr({{:., _, [remote, name]}, _meta, args}, stack, context)
when is_atom(name) do
context = of_expr(remote, stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# &Mod.fun/arity
def of_expr({:&, _, [{:/, _, [{{:., _, [_remote, name]}, _, []}, arity]}]}, _stack, context)
when is_atom(name) and is_integer(arity) do
context
end
# &fun/arity
def of_expr({:&, meta, [{:/, _, [{name, _, _ctx}, arity]}]}, stack, context)
when is_atom(name) and is_integer(arity) do
local_fun(meta, name, arity, stack, context)
end
# super(args)
def of_expr({:super, meta, args}, stack, context) when is_list(args) do
{_kind, name} = Keyword.fetch!(meta, :super)
context = local_fun(meta, name, length(args), stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# local_fun(args)
def of_expr({name, meta, args}, stack, context)
when is_atom(name) and is_list(args) do
context = local_fun(meta, name, length(args), stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# var
def of_expr({name, _meta, ctx}, _stack, context)
when is_atom(name) and is_atom(ctx) do
context
end
defp of_pattern({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) when is_atom(module) do
context = of_pattern(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
defp of_pattern({left, _meta, right}, stack, context) do
context = of_pattern(left, stack, context)
of_pattern(right, stack, context)
end
defp of_pattern({left, right}, stack, context) do
context = of_pattern(left, stack, context)
of_pattern(right, stack, context)
end
defp of_pattern([_ | _] = list, stack, context) do
Enum.reduce(list, context, &of_pattern(&1, stack, &2))
end
defp of_pattern(_, _stack, context) do
context
end
defp of_struct(module, pairs, kind, meta, stack, context) do
{info, context} = Module.Types.Of.struct_info(module, kind, meta, stack, context)
if info do
Enum.reduce(pairs, context, fn {key, _value}, context ->
if Enum.any?(info, &(&1.field == key)) do
context
else
Module.Types.Of.unknown_struct_field(module, key, kind, meta, stack, context)
end
end)
else
context
end
end
defp local_fun(meta, fun, arity, stack, context) do
case stack.local_handler.(meta, {fun, arity}, stack, context) do
false -> context
{_kind, _info, context} -> context
end
end
end
+2 -8
View File
@@ -952,14 +952,8 @@ defmodule OptionParser do
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
case to_switch(name) do
"--no-" <> _ = switch ->
add_aliases(switch, name, reverse_aliases)
switch ->
base = "#{switch}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
end
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
+8 -10
View File
@@ -170,8 +170,6 @@ defmodule PartitionSupervisor do
| {:max_seconds, non_neg_integer()}
| {:with_arguments, (args :: [term()], partition() -> updated_args :: [term()])}
defguardp is_name(name) when is_atom(name) or elem(name, 0) == :via
@doc false
def child_spec(opts) when is_list(opts) do
id =
@@ -368,7 +366,7 @@ defmodule PartitionSupervisor do
"""
@doc since: "1.18.0"
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
def resize!(name, partitions) when is_name(name) and is_integer(partitions) do
def resize!(name, partitions) when is_integer(partitions) do
supervisor =
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
@@ -423,8 +421,8 @@ defmodule PartitionSupervisor do
Returns the number of partitions for the partition supervisor.
"""
@doc since: "1.14.0"
@spec partitions(name()) :: non_neg_integer()
def partitions(name) when is_name(name) do
@spec partitions(name()) :: pos_integer()
def partitions(name) do
name |> table() |> partitions(name)
end
@@ -452,7 +450,7 @@ defmodule PartitionSupervisor do
end
@doc """
Returns a list with information about all children of the given supervisor.
Returns a list with information about all children.
This function returns a list of tuples containing:
@@ -472,7 +470,7 @@ defmodule PartitionSupervisor do
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_name(name) do
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
Supervisor.which_children(name)
end
@@ -500,7 +498,7 @@ defmodule PartitionSupervisor do
supervisors: non_neg_integer,
workers: non_neg_integer
}
def count_children(supervisor) when is_name(supervisor) do
def count_children(supervisor) when is_atom(supervisor) do
Supervisor.count_children(supervisor)
end
@@ -516,7 +514,7 @@ defmodule PartitionSupervisor do
"""
@doc since: "1.14.0"
@spec stop(name(), reason :: term, timeout) :: :ok
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do
Supervisor.stop(supervisor, reason, timeout)
end
@@ -548,7 +546,7 @@ defmodule PartitionSupervisor do
end
@doc false
def unregister_name(_) do
def unregister_name(_, _) do
raise "{:via, PartitionSupervisor, _} cannot be given on unregistration"
end
end
+1 -1
View File
@@ -485,7 +485,7 @@ defmodule Path do
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
_ -> IO.chardata_to_string(path)
_ -> path
end
end
+27 -43
View File
@@ -202,23 +202,24 @@ defmodule Process do
@doc """
Sends an exit signal with the given `reason` to `pid`.
Exit behavior differs based on the value of `reason`:
The following behavior applies if `reason` is any term except `:normal`
or `:kill`:
- If `:normal`, `pid` will not exit unless it is the calling process, in
which case it will exit with the reason `:normal`. If it is trapping exits,
the exit signal is transformed into a message `{:EXIT, from, :normal}` and
delivered to its message queue.
1. If `pid` is not trapping exits, `pid` will exit with the given
`reason`.
- If `:kill`, which occurs when `Process.exit(pid, :kill)` is called, an
untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
2. If `pid` is trapping exits, the exit signal is transformed into a
message `{:EXIT, from, reason}` and delivered to the message queue
of `pid`.
- If any other term and `pid` is not trapping exits, `pid` will exit with
the given `reason`.
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
the calling process, in which case it will exit with the reason `:normal`).
If it is trapping exits, the exit signal is transformed into a message
`{:EXIT, from, :normal}` and delivered to its message queue.
- If any other term and `pid` is trapping exits, the exit signal is
transformed into a message `{:EXIT, from, reason}` and delivered to its
message queue.
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
an untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
Inlined by the compiler.
@@ -387,6 +388,8 @@ defmodule Process do
automatically canceled when `dest` is an atom (as the atom resolution is done
on delivery).
Inlined by the compiler.
## Options
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
@@ -532,7 +535,7 @@ defmodule Process do
If the process is already dead when calling `Process.monitor/1`, a
`:DOWN` message is delivered immediately.
See ["Links and monitors"](genservers.md#links-and-monitors)
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
for an example. See `:erlang.monitor/2` for more information.
Inlined by the compiler.
@@ -837,7 +840,7 @@ defmodule Process do
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:priority, priority_level) :: priority_level
@spec flag(:save_calls, 0..10_000) :: 0..10_000
@spec flag(:save_calls, 0..10000) :: 0..10000
@spec flag(:sensitive, boolean) :: boolean
@spec flag(:trap_exit, boolean) :: boolean
defdelegate flag(flag, value), to: :erlang, as: :process_flag
@@ -856,7 +859,7 @@ defmodule Process do
Inlined by the compiler.
"""
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
@spec flag(pid, :save_calls, 0..10000) :: 0..10000
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
@doc """
@@ -977,35 +980,11 @@ defmodule Process do
@spec unalias(alias) :: boolean
defdelegate unalias(alias), to: :erlang
@doc """
Returns the label set for the process `pid` as set with `set_label/1`
or `:proc_lib.set_label/1`.
Defaults to the current process when `pid` is not passed.
## Examples
Process.set_label({:any, "term"})
Process.get_label()
#=> {:any, "term"}
Returns `nil` when not set:
Process.get_label(pid)
#=> nil
"""
@doc since: "1.20.0"
@spec get_label(pid()) :: term()
def get_label(pid \\ self()) do
nilify(:proc_lib.get_label(pid))
end
@doc """
Add a descriptive term to the current process.
The term does not need to be unique, and will be shown in Observer and in
crash logs.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
This label may be useful for identifying a process as one of multiple in a
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
@@ -1019,7 +998,12 @@ defmodule Process do
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
defdelegate set_label(label), to: :proc_lib
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil
+95 -125
View File
@@ -21,7 +21,7 @@ defmodule Protocol do
the data structure.
Although Elixir includes specific functions such as `tuple_size`,
`byte_size` and `map_size`, sometimes we want to be able to
`binary_size` and `map_size`, sometimes we want to be able to
retrieve the size of a data structure regardless of its type.
In Elixir we can write polymorphic code, i.e. code that works
with different shapes/types, by using protocols. A size protocol
@@ -287,11 +287,6 @@ defmodule Protocol do
call_args = :lists.map(to_var, :lists.seq(2, arity))
call_args = [quote(do: term) | call_args]
# TODO: Raise in Elixir v2.0
if :lists.any(&match?({:\\, _, [_, _]}, &1), args) do
IO.warn("default arguments in protocol definitions is deprecated", __CALLER__)
end
quote generated: true do
name = unquote(name)
arity = unquote(arity)
@@ -705,7 +700,7 @@ defmodule Protocol do
{Descr.term(), clauses, clauses}
else
{domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
{domain, [{[Descr.term()], Descr.atom()}], clauses}
end
end
@@ -786,7 +781,7 @@ defmodule Protocol do
end
defp fallback_clause_for(value, _protocol, meta) do
{meta, [{:_, [version: -1], __MODULE__}], [], value}
{meta, [quote(do: _)], [], value}
end
# Finally compile the module and emit its bytecode.
@@ -805,7 +800,8 @@ defmodule Protocol do
@before_compile Protocol
# We don't allow function definition inside protocols
import Kernel, except: [def: 1, def: 2]
import Kernel,
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
# Import the new `def` that is used by protocols
import Protocol, only: [def: 1]
@@ -816,11 +812,12 @@ defmodule Protocol do
# Set up a clear slate to store defined functions
@__functions__ []
@fallback_to_any false
@undefined_impl_description ""
res = unquote(block)
# Invoke the user given block
_ = unquote(block)
# Finalize expansion
unquote(after_defprotocol())
res
end
end
end
@@ -867,21 +864,6 @@ defmodule Protocol do
)
end
extra =
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
functions) --
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
# TODO: Make an error on Elixir v2.0
if extra != [] do
warn(
"protocols can only define functions without implementation via def/1, found: " <>
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
env,
nil
)
end
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
@@ -926,105 +908,57 @@ defmodule Protocol do
end
defp after_defprotocol do
prefix =
quote bind_quoted: [built_in: built_in()] do
any_impl_for =
if @fallback_to_any do
Protocol.__concat__(__MODULE__, "Any")
else
nil
end
# Disable Dialyzer checks - before and after consolidation
# the types could be more strict
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
@doc false
@spec impl_for(term) :: atom | nil
Kernel.def(impl_for(data))
# Define the implementation for structs.
#
# It simply delegates to struct_impl_for which is then
# optimized during protocol consolidation.
Kernel.def impl_for(%struct{}) do
struct_impl_for(struct)
quote bind_quoted: [built_in: built_in()] do
any_impl_for =
if @fallback_to_any do
__MODULE__.Any
else
nil
end
# Define the implementation for built-ins
:lists.foreach(
fn {mod, guard} ->
target = Protocol.__concat__(__MODULE__, mod)
# Disable Dialyzer checks - before and after consolidation
# the types could be more strict
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
@doc false
@spec impl_for(term) :: atom | nil
Kernel.def(impl_for(data))
# Define the implementation for structs.
#
# It simply delegates to struct_impl_for which is then
# optimized during protocol consolidation.
Kernel.def impl_for(%struct{}) do
struct_impl_for(struct)
end
# Define the implementation for built-ins
:lists.foreach(
fn {mod, guard} ->
target = Protocol.__concat__(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end,
built_in
)
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
end,
built_in
)
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@type t :: term
end
# Store information as an attribute so it
# can be read without loading the module.
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
@__protocol__ [fallback_to_any: !!@fallback_to_any]
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
Kernel.def(__protocol__(:impls), do: :not_consolidated)
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
Kernel.def impl_for(_) do
unquote(any_impl_for)
end
raise =
quote do
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: @undefined_impl_description
)
end
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
impl_for_fallback =
quote generated: true, bind_quoted: [] do
Kernel.def impl_for(_) do
unquote(any_impl_for)
end
end
quote generated: true do
unquote(prefix)
unquote(impl_for_fallback)
undefined_impl_description =
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
@doc false
@spec impl_for!(term) :: atom
@@ -1034,9 +968,47 @@ defmodule Protocol do
end
else
Kernel.def impl_for!(data) do
impl_for(data) || unquote(raise)
impl_for(data) ||
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: unquote(undefined_impl_description)
)
end
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@type t :: term
end
# Store information as an attribute so it
# can be read without loading the module.
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
@__protocol__ [fallback_to_any: !!@fallback_to_any]
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
Kernel.def(__protocol__(:impls), do: :not_consolidated)
end
end
@@ -1223,12 +1195,10 @@ defmodule Protocol do
end
@doc false
def __concat__(left, right) when is_atom(right) do
__concat__(left, remove_prefix(Atom.to_string(right)))
end
def __concat__(left, right) when is_binary(right) do
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
+9 -6
View File
@@ -207,6 +207,12 @@ defmodule Range do
%Range{first: first, last: last, step: step}
end
def new(first, last) do
raise ArgumentError,
"ranges (first..last) expect both sides to be integers, " <>
"got: #{inspect(first)}..#{inspect(last)}"
end
@doc """
Creates a new range with `step`.
@@ -284,12 +290,9 @@ defmodule Range do
"""
@doc since: "1.14.0"
@spec shift(t, integer) :: t
def shift(%Range{} = range, 0), do: range
def shift(first..last//step, steps_to_shift) when is_integer(steps_to_shift) do
shift = steps_to_shift * step
new(first + shift, last + shift, step)
def shift(first..last//step, steps_to_shift)
when is_integer(steps_to_shift) do
new(first + steps_to_shift * step, last + steps_to_shift * step, step)
end
@doc """
+5 -33
View File
@@ -78,7 +78,8 @@ defmodule Regex do
* `:caseless` (i) - adds case insensitivity
* `:dotall` (s) - causes dot to match newlines and also sets newline to `(*ANYCRLF)`.
* `:dotall` (s) - causes dot to match newlines and also sets newline to
`(*ANYCRLF)`.\
The new line setting, as described in the [`:re` documentation](`:re`),
can be overridden by starting the regular expression pattern with:
* `(*CR)` - carriage return
@@ -86,7 +87,7 @@ defmodule Regex do
* `(*CRLF)` - carriage return, followed by line feed
* `(*ANYCRLF)` - any of the three above
* `(*ANY)` - all Unicode newline sequences
* `(*NUL)` - the NUL character (binary zero) (from Erlang/OTP 28)
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
each line; use `\A` and `\z` to match the end or beginning of the string
@@ -101,8 +102,8 @@ defmodule Regex do
(the previous `r` option is deprecated in favor of `U`)
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
which can be shared across nodes or passed through config, at the cost of a runtime
overhead to re-import it every time it is executed.
which can be shared across nodes or through config, at the cost of a runtime
overhead every time to re-import it every time it is executed.
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
and do not need to be exported in order to be shared in these versions.
@@ -274,35 +275,6 @@ defmodule Regex do
regex
end
@doc """
Imports a `regex` that has been exported, otherwise returns the `regex` unchanged.
This means it will lose the ability to be sent across nodes or passed through config,
but will be faster since it won't need to be imported on the fly every time it is executed.
Exported regexes only exist on OTP 28, so this has no effect on older versions.
## Examples
Regex.import(~r/foo/E)
~r/foo/
Regex.import(~r/foo/)
~r/foo/
"""
@doc since: "1.20.0"
@spec import(t) :: t
def import(%Regex{re_pattern: re_pattern} = regex) do
case re_pattern do
{:re_exported_pattern, _, _, _, _} ->
%{regex | re_pattern: :re.import(re_pattern), opts: regex.opts -- [:export]}
_ ->
regex
end
end
@doc """
Returns the version of the underlying Regex engine.
"""
+88 -204
View File
@@ -258,7 +258,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case lookup_second(:unique, key_ets, key) do
case safe_lookup_second(key_ets, key) do
{pid, _} ->
if Process.alive?(pid), do: pid, else: :undefined
@@ -346,8 +346,7 @@ defmodule Registry do
The registry requires the following keys:
* `:keys` - chooses if keys are `:unique`, `:duplicate`,
`{:duplicate, :key}`, or `{:duplicate, :pid}`
* `:keys` - chooses if keys are `:unique`, `:duplicate`, `{:duplicate, :key}`, or `{:duplicate, :pid}`
* `:name` - the name of the registry and its tables
The following keys are optional:
@@ -359,20 +358,17 @@ defmodule Registry do
crashes. Messages sent to listeners are of type `t:listener_message/0`.
* `:meta` - a keyword list of metadata to be attached to the registry.
For `:duplicate` registries, you can specify the partitioning strategy
directly in the `:keys` option:
For `:duplicate` registries, you can specify the partitioning strategy
directly in the `:keys` option:
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
when you have keys with many entries (e.g., one topic with many subscribers).
This is the traditional behavior and groups all entries from the same process together.
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
when you have keys with many entries (e.g., one topic with many subscribers).
This is the traditional behavior and groups all entries from the same process together.
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
many different keys (e.g., many topics with few subscribers each). This makes
key-based lookups more efficient as they only need to check a single partition
instead of all partitions. This option uses a different internal ETS table type
(`ordered_set` instead of `duplicate_bag`), which means match specs passed to
`select/2` and `count_select/2` that reference `:"$_"` may behave differently.
Use named match variables (`:"$1"`, `:"$2"`, etc.) instead.
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
many different keys (e.g., many topics with few subscribers each). This makes
key-based lookups more efficient as they only need to check a single partition
instead of all partitions.
"""
@doc since: "1.5.0"
@@ -535,17 +531,13 @@ defmodule Registry do
given as an option, the dispatching happens in parallel. In both cases,
the callback is only invoked if there are entries for that partition.
For `{:duplicate, :key}` registries, all entries for a given key are in a
single partition, so the `:parallel` option has no effect.
See the module documentation for examples of using the `dispatch/3`
function for building custom dispatching or a pubsub system.
## Options
* `:parallel` - if `true`, the dispatching is done in parallel
across all partitions. Only meaningful for `{:duplicate, :pid}`
registries. Defaults to `false`.
across all partitions. Defaults to `false`.
"""
@doc since: "1.4.0"
@@ -556,24 +548,17 @@ defmodule Registry do
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:unique
|> lookup_second(key_ets, key)
(key_ets || key_ets!(registry, key, partitions))
|> safe_lookup_second(key)
|> List.wrap()
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, _} = kind, 1, key_ets} ->
kind
|> lookup_second(key_ets, key)
{{:duplicate, _}, 1, key_ets} ->
key_ets
|> safe_lookup_second(key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, :key}, partitions, _} ->
{:duplicate, :key}
|> lookup_second(key_ets!(registry, key, partitions), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, :pid}, partitions, _} ->
{{:duplicate, _}, partitions, _} ->
if Keyword.get(opts, :parallel, false) do
registry
|> dispatch_parallel(key, mfa_or_fun, partitions)
@@ -593,8 +578,9 @@ defmodule Registry do
defp dispatch_serial(registry, key, mfa_or_fun, partition) do
partition = partition - 1
{:duplicate, :pid}
|> lookup_second(key_ets!(registry, partition), key)
registry
|> key_ets!(partition)
|> safe_lookup_second(key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
dispatch_serial(registry, key, mfa_or_fun, partition)
@@ -610,8 +596,9 @@ defmodule Registry do
task =
Task.async(fn ->
{:duplicate, :pid}
|> lookup_second(key_ets!(registry, partition), key)
registry
|> key_ets!(partition)
|> safe_lookup_second(key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
Process.unlink(parent)
@@ -675,7 +662,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case lookup_second(:unique, key_ets, key) do
case safe_lookup_second(key_ets, key) do
{_, _} = pair ->
[pair]
@@ -683,15 +670,16 @@ defmodule Registry do
[]
end
{{:duplicate, _} = kind, 1, key_ets} ->
lookup_second(kind, key_ets, key)
{{:duplicate, _}, 1, key_ets} ->
safe_lookup_second(key_ets, key)
{{:duplicate, :key}, partitions, _key_ets} ->
lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key)
partition = hash(key, partitions)
safe_lookup_second(key_ets!(registry, partition), key)
{{:duplicate, :pid}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key),
pair <- safe_lookup_second(key_ets!(registry, partition), key),
do: pair
end
end
@@ -802,18 +790,21 @@ defmodule Registry do
@doc since: "1.4.0"
@spec match(registry, key, match_pattern, guards) :: [{pid, term}]
def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do
case key_info!(registry) do
{{:duplicate, :pid}, partitions, nil} ->
spec = match_spec({:duplicate, :pid}, key, pattern, guards)
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
{{:duplicate, _}, 1, key_ets} ->
:ets.select(key_ets, spec)
{{:duplicate, _}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- :ets.select(key_ets!(registry, partition), spec),
do: pair
{kind, partitions, key_ets} ->
spec = match_spec(kind, key, pattern, guards)
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
end
end
@@ -951,7 +942,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case lookup_second(:unique, key_ets, key) do
case safe_lookup_second(key_ets, key) do
{^pid, value} ->
[value]
@@ -959,17 +950,18 @@ defmodule Registry do
[]
end
{{:duplicate, _} = kind, 1, key_ets} ->
for {^pid, value} <- lookup_second(kind, key_ets, key), do: value
{{:duplicate, _}, 1, key_ets} ->
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, :key}, partitions, _key_ets} ->
key_ets = key_ets!(registry, key, partitions)
for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value
partition = hash(key, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, :pid}, partitions, _key_ets} ->
partition = hash(pid, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
end
end
@@ -1023,8 +1015,7 @@ defmodule Registry do
# Remove first from the key_ets because in case of crashes
# the pid_ets will still be able to clean up. The last step is
# to clean if we have no more entries.
true = __unregister__(key_ets, {key, {self, :_}}, 1, ordered?(kind))
true = __unregister__(key_ets, {key, {self, :_}}, 1)
true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2)
unlink_if_unregistered(pid_server, pid_ets, self)
@@ -1089,10 +1080,13 @@ defmodule Registry do
# to clean if we have no more entries.
# Here we want to count all entries for this pid under this key, regardless of pattern.
{total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards)
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
total = :ets.select_count(key_ets, total_spec)
# We only want to delete things that match the pattern
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]
case :ets.select_delete(key_ets, delete_spec) do
# We deleted everything, we can just delete the object
^total ->
@@ -1126,25 +1120,6 @@ defmodule Registry do
:ok
end
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
else
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
end
end
defp unregister_match_specs(_kind, key, self, pattern, guards) do
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
end
@doc """
Registers the current process under the given `key` in `registry`.
@@ -1204,14 +1179,7 @@ defmodule Registry do
counter = System.unique_integer()
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
key_entry =
if ordered?(kind) do
{{key, self, counter}, value}
else
{key, {self, value}}
end
case register_key(kind, key_ets, key, key_entry) do
case register_key(kind, key_ets, key, {key, {self, value}}) do
:ok ->
for listener <- listeners do
Kernel.send(listener, {:register, registry, key, self, value})
@@ -1440,18 +1408,21 @@ defmodule Registry do
@spec count_match(registry, key, match_pattern, guards) :: non_neg_integer()
def count_match(registry, key, pattern, guards \\ [])
when is_atom(registry) and is_list(guards) do
case key_info!(registry) do
{{:duplicate, :pid}, partitions, nil} ->
spec = count_match_spec({:duplicate, :pid}, key, pattern, guards)
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [true]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
{{:duplicate, _}, 1, key_ets} ->
:ets.select_count(key_ets, spec)
{{:duplicate, _}, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{kind, partitions, key_ets} ->
spec = count_match_spec(kind, key, pattern, guards)
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
end
end
@@ -1480,9 +1451,6 @@ defmodule Registry do
some operations like `:element` to modify the output format.
Do not use special match variables `:"$_"` and `:"$$"`, because they might not work as expected.
In particular, `{:duplicate, :key}` registries use a different internal ETS layout, so match specs
that reference the underlying entry structure via `:"$_"` will return different results.
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
Note that for large registries with many partitions this will be costly as it builds the result by
concatenating all the partitions.
@@ -1510,16 +1478,15 @@ defmodule Registry do
@spec select(registry, spec) :: [term]
def select(registry, spec)
when is_atom(registry) and is_list(spec) do
case key_info!(registry) do
{kind, partitions, nil} ->
spec = group_match_headers(spec, kind, __ENV__.function)
spec = group_match_headers(spec, __ENV__.function)
case key_info!(registry) do
{_kind, partitions, nil} ->
Enum.flat_map(0..(partitions - 1), fn partition_index ->
:ets.select(key_ets!(registry, partition_index), spec)
end)
{kind, 1, key_ets} ->
spec = group_match_headers(spec, kind, __ENV__.function)
{_kind, 1, key_ets} ->
:ets.select(key_ets, spec)
end
end
@@ -1542,30 +1509,24 @@ defmodule Registry do
@spec count_select(registry, spec) :: non_neg_integer()
def count_select(registry, spec)
when is_atom(registry) and is_list(spec) do
case key_info!(registry) do
{kind, partitions, nil} ->
spec = group_match_headers(spec, kind, __ENV__.function)
spec = group_match_headers(spec, __ENV__.function)
case key_info!(registry) do
{_kind, partitions, nil} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{kind, 1, key_ets} ->
spec = group_match_headers(spec, kind, __ENV__.function)
{_kind, 1, key_ets} ->
:ets.select_count(key_ets, spec)
end
end
defp group_match_headers(spec, kind, {fun, arity}) do
ordered = ordered?(kind)
defp group_match_headers(spec, {fun, arity}) do
for part <- spec do
case part do
{{key, pid, value}, guards, body} when ordered ->
{{{key, pid, :_}, value}, guards, body}
{{key, pid, value}, guards, body} ->
{{key, {pid, value}}, guards, body}
{{key, pid, value}, guards, select} ->
{{key, {pid, value}}, guards, select}
_ ->
raise ArgumentError,
@@ -1616,23 +1577,7 @@ defmodule Registry do
:ets.lookup_element(registry, partition, 3)
end
defp lookup_second({:duplicate, :key}, ets, key) do
spec =
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
else
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
end
try do
:ets.select(ets, spec)
catch
:error, :badarg -> []
end
end
defp lookup_second(_kind, ets, key) do
defp safe_lookup_second(ets, key) do
try do
:ets.lookup_element(ets, key, 2)
catch
@@ -1661,22 +1606,10 @@ defmodule Registry do
end
@doc false
def __unregister__(table, match, pos, ordered \\ false)
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
else
:ets.match_delete(table, {{key, pid, :_}, :_})
end
end
def __unregister__(table, match, pos, false = _ordered) do
def __unregister__(table, match, pos) do
key = :erlang.element(pos, match)
# We need to perform an element comparison if we have a special atom key.
# We need to perform an element comparison if we have an special atom key.
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
match = :erlang.setelement(pos, match, :_)
guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}}
@@ -1686,52 +1619,9 @@ defmodule Registry do
end
end
# For {:duplicate, :key} registries, the ETS entry layout is
# {{key, pid, counter}, value} (ordered_set with composite key),
# so :"$_" refers to that whole tuple. The body extracts {pid, value}
# to match the public API's return shape of [{pid, value}].
defp match_spec({:duplicate, :key}, key, pattern, guards) do
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, body}]
else
[{{{key, :_, :_}, pattern}, guards, body}]
end
end
defp match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
end
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, [true]}]
else
[{{{key, :_, :_}, pattern}, guards, [true]}]
end
end
defp count_match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [true]}]
end
defp reserved_atom?("_"), do: true
defp reserved_atom?("$" <> _), do: true
defp reserved_atom?(_), do: false
defp ordered?({:duplicate, :key}), do: true
defp ordered?(_), do: false
end
defmodule Registry.Supervisor do
@@ -1843,8 +1733,7 @@ defmodule Registry.Partition do
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
end
ordered = kind == {:duplicate, :key}
{:ok, {pid_ets, %{}, ordered}}
{:ok, {pid_ets, %{}}}
end
# The key partition is a set for unique keys,
@@ -1854,12 +1743,7 @@ defmodule Registry.Partition do
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets({:duplicate, :key}, key_partition, compressed) do
opts = [:ordered_set, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets({:duplicate, :pid}, key_partition, compressed) do
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
@@ -1883,7 +1767,7 @@ defmodule Registry.Partition do
{:reply, :ok, state}
end
def handle_call({:lock, key}, from, {ets, lock, ordered}) do
def handle_call({:lock, key}, from, {ets, lock}) do
lock =
case lock do
%{^key => queue} ->
@@ -1894,10 +1778,10 @@ defmodule Registry.Partition do
Map.put(lock, key, :queue.new())
end
{:noreply, {ets, lock, ordered}}
{:noreply, {ets, lock}}
end
def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
entries = :ets.take(ets, pid)
for {_pid, key, key_ets, _counter} <- entries do
@@ -1912,13 +1796,13 @@ defmodule Registry.Partition do
end
try do
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered)
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1)
catch
:error, :badarg -> :badarg
end
end
{:noreply, {ets, lock, ordered}}
{:noreply, {ets, lock}}
end
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
@@ -1930,7 +1814,7 @@ defmodule Registry.Partition do
unlock(key, state)
end
defp unlock(key, {ets, lock, ordered}) do
defp unlock(key, {ets, lock}) do
%{^key => queue} = lock
lock =
@@ -1939,7 +1823,7 @@ defmodule Registry.Partition do
{:not_empty, queue} -> Map.put(lock, key, queue)
end
{:noreply, {ets, lock, ordered}}
{:noreply, {ets, lock}}
end
defp dequeue(queue, key) do
+2 -18
View File
@@ -1437,7 +1437,7 @@ defmodule Stream do
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
{_, []} ->
do_cycle(check_cycle_subsequent_element(cycle), [], cycle, {:cont, acc}, fun)
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
end
end
@@ -1446,26 +1446,10 @@ defmodule Stream do
end
defp check_cycle_first_element(reduce) do
check_cycle_non_empty(
reduce,
ArgumentError,
"cannot cycle over an empty enumerable"
)
end
defp check_cycle_subsequent_element(reduce) do
check_cycle_non_empty(
reduce,
RuntimeError,
"cycled enumerable became empty after a previous iteration produced elements"
)
end
defp check_cycle_non_empty(reduce, exception, message) do
fn acc ->
case reduce.(acc) do
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
raise exception, message
raise ArgumentError, "cannot cycle over an empty enumerable"
other ->
other
+12 -64
View File
@@ -22,7 +22,7 @@ defmodule String do
"hello world"
The functions in this module act according to
[The Unicode Standard, Version 17.0.0](https://www.unicode.org/versions/Unicode17.0.0/).
[The Unicode Standard, Version 17.0.0](http://www.unicode.org/versions/Unicode17.0.0/).
## Interpolation
@@ -483,7 +483,7 @@ defmodule String do
iex> String.split("abc", "", parts: 3)
["", "a", "bc"]
Splitting on a non-existing pattern returns the original string:
Splitting on an non-existing pattern returns the original string:
iex> String.split("abc", ",")
["abc"]
@@ -745,15 +745,7 @@ defmodule String do
iex> String.count("hello world", "")
12
The `pattern` can also be a list:
iex> String.count("hello world", ["e", "o"])
3
iex> String.count("hello world", [])
0
Or a compiled pattern:
The `pattern` can also be a compiled pattern:
iex> pattern = :binary.compile_pattern([" ", "!"])
iex> String.count("foo bar baz!!", pattern)
@@ -764,8 +756,6 @@ defmodule String do
@doc since: "1.19.0"
def count(string, <<>>), do: length(string) + 1
def count(_string, []), do: 0
def count(string, pattern) when is_struct(pattern, Regex) do
Kernel.length(Regex.scan(pattern, string, return: :index))
end
@@ -1008,10 +998,6 @@ defmodule String do
IO.iodata_to_binary(downcase_ascii(string))
end
def downcase(string, :greek) when is_binary(string) do
String.Unicode.downcase_greek(string, [])
end
def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
String.Unicode.downcase(string, [], mode)
end
@@ -1062,39 +1048,19 @@ defmodule String do
@letter_I_dot_above <<0x0130::utf8>>
def capitalize(<<@letter_i, right::binary>>, mode) do
capitalized = if mode == :turkic, do: @letter_I_dot_above, else: @letter_I
capitalized <> downcase_after_capitalize(right, mode, ?i)
if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode)
end
def capitalize(string, mode) when is_binary(string) do
case :unicode_util.gc(string) do
[gc] ->
grapheme_to_binary(:string.titlecase([gc]))
[gc, rest] ->
grapheme_to_binary(:string.titlecase([gc])) <>
downcase_after_capitalize(rest, mode, gc)
[gc | rest] ->
grapheme_to_binary(:string.titlecase([gc])) <>
downcase_after_capitalize(rest, mode, gc)
[] ->
""
{:error, <<byte, rest::bits>>} ->
<<byte>> <> downcase(rest, mode)
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
[] -> ""
{:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode)
end
end
defp downcase_after_capitalize(rest, :greek, gc) do
String.Unicode.downcase_greek(rest, [], grapheme_to_binary(gc))
end
defp downcase_after_capitalize(rest, mode, _gc) do
downcase(rest, mode)
end
@doc false
@deprecated "Use String.trim_trailing/1 instead"
defdelegate rstrip(binary), to: String.Break, as: :trim_trailing
@@ -2324,19 +2290,6 @@ defmodule String do
end
end
# 56-bit SWAR guard: all 7 bytes are ASCII (< 128) and none is \r (0x0D).
# Uses Mycroft's zero-byte detection: XOR with 0x0D and check for zero bytes.
defguardp ascii_no_cr_swar?(w)
when Bitwise.band(w, 0x80808080808080) == 0 and
Bitwise.band(
Bitwise.bxor(w, 0x0D0D0D0D0D0D0D) - 0x01010101010101,
0x80808080808080
) == 0
defp skip_length(<<w::56, b, rest::binary>>, acc)
when b <= 127 and b != ?\r and ascii_no_cr_swar?(w),
do: skip_length(rest, acc + 8)
defp skip_length(<<byte, rest::binary>>, acc)
when byte <= 127 and byte != ?\r,
do: skip_length(rest, acc + 1)
@@ -3215,7 +3168,8 @@ defmodule String do
def jaro_distance("", _string), do: 0.0
def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
:string.jaro_similarity(string1, string2)
# TODO: Replace by :string.jaro_similarity/2 when we require Erlang/OTP 27+
:elixir_utils.jaro_similarity(string1, string2)
end
@doc """
@@ -3260,13 +3214,7 @@ defmodule String do
byte_size_unicode(unicode)
end
defp byte_size_remaining_at(<<byte1, byte2, rest::binary>> = binary, n)
when n > 0 and byte1 <= 127 and byte1 != ?\r and byte2 <= 127 and byte2 != ?\r do
skip = min(skip_length(rest, 1), n)
byte_size_remaining_at(binary_part(binary, skip, byte_size(binary) - skip), n - skip)
end
defp byte_size_remaining_at(unicode, n) when n > 0 do
defp byte_size_remaining_at(unicode, n) do
case :unicode_util.gc(unicode) do
[_] -> 0
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
+14 -36
View File
@@ -556,29 +556,19 @@ defmodule Supervisor do
Developers typically invoke `Supervisor.init/2` at the end of their
init callback to return the proper supervision flags.
See `start_link/2` for more information.
"""
@callback init(init_arg :: term) ::
{:ok,
{sup_flags(), [child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
| :ignore
@typedoc """
Return values of `start_link/2` and `start_link/3`.
See the documentation for those functions for more information.
"""
@typedoc "Return values of `start_link/2` and `start_link/3`."
@type on_start ::
{:ok, pid}
| :ignore
| {:error, {:already_started, pid} | {:shutdown, term} | term}
@typedoc """
Return values of `start_child/2`.
See the documentation for those functions for more information.
"""
@typedoc "Return values of `start_child/2`."
@type on_start_child ::
{:ok, child}
| {:ok, child, info :: term}
@@ -708,18 +698,15 @@ defmodule Supervisor do
If the supervisor and all child processes are successfully spawned
(if the start function of each child process returns `{:ok, child}`,
`{:ok, child, info}`, or `:ignore`), this function returns
`{:ok, pid}`, where `pid` is the PID of the supervisor.
`{:ok, pid}`, where `pid` is the PID of the supervisor. If the supervisor
is given a name and a process with the specified name already exists,
the function returns `{:error, {:already_started, pid}}`, where `pid`
is the PID of that process.
If the supervisor is given a name and a process with the specified name
already exists, the function returns `{:error, {:already_started, pid}}`,
where `pid` is the PID of that process.
If the start function of any of the child processes fails or returns
an error tuple or an erroneous value, the supervisor first terminates
with reason `:shutdown` all the child processes that have already been
started, and then terminates itself. If the caller process is trapping
exits, this function then returns
`{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
If the start function of any of the child processes fails or returns an error
tuple or an erroneous value, the supervisor first terminates with reason
`:shutdown` all the child processes that have already been started, and then
terminates itself and returns `{:error, {:shutdown, reason}}`.
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
@@ -952,17 +939,9 @@ defmodule Supervisor do
If the `c:init/1` callback returns `:ignore`, this function returns
`:ignore` as well and the supervisor terminates with reason `:normal`.
If the `c:init/1` callback fails or returns an incorrect value, the supervisor
will shutdown with reason `term`, where `term` contains information
about the error. If the caller process is trapping exits, this function then
returns `{:error, term()}`.
If the start function of any of the child processes returned by `c:init/1`
fail or return an error tuple or an erroneous value, the supervisor first
terminates with reason `:shutdown` all the child processes that have already
been started, and then terminates itself. If the caller process is trapping
exits, this function then returns `{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
If it fails or returns an incorrect value, this function returns
`{:error, term}` where `term` is a term with information about the
error, and the supervisor terminates with reason `term`.
The `:name` option can also be given in order to register a supervisor
name, the supported values are described in the "Name registration"
@@ -1132,8 +1111,7 @@ defmodule Supervisor do
Returns a list with information about all children of the given supervisor.
Note that calling this function when supervising a large number of children
under low memory conditions can bring the system down due to an out of memory
error.
under low memory conditions can cause an out of memory exception.
This function returns a list of `{id, child, type, modules}` tuples, where:
+19 -20
View File
@@ -1040,6 +1040,25 @@ defmodule System do
> such as `.exe`, and double check the program is indeed not a batch
> file or `.com` application.
## Examples
iex> System.cmd("echo", ["hello"])
{"hello\n", 0}
iex> System.cmd("echo", ["hello"], env: [{"MIX_ENV", "test"}])
{"hello\n", 0}
If you want to stream the output to Standard IO as it arrives:
iex> System.cmd("echo", ["hello"], into: IO.stream())
hello
{%IO.Stream{}, 0}
If you want to read lines:
iex> System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
{["hello", "world"], 0}
## Options
* `:into` - injects the result into the given collectable, defaults to `""`
@@ -1103,26 +1122,6 @@ defmodule System do
If you desire to execute a trusted command inside a shell, with pipes,
redirecting and so on, please check `shell/2`.
## Examples
iex> System.cmd("echo", ["hello"])
{"hello\n", 0}
iex> System.cmd("echo", ["hello"], env: [{"MIX_ENV", "test"}])
{"hello\n", 0}
If you want to stream the output to Standard IO as it arrives:
iex> System.cmd("echo", ["hello"], into: IO.stream())
hello
{%IO.Stream{}, 0}
If you want to read lines:
iex> System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
{["hello", "world"], 0}
"""
@spec cmd(binary, [binary], cmd_opts) :: {Collectable.t(), exit_status :: non_neg_integer}
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
+7 -10
View File
@@ -105,7 +105,7 @@ defmodule Task do
], strategy: :one_for_one)
And now you can use async/await by passing the name of
the supervisor instead of the PID:
the supervisor instead of the pid:
Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
# Do something
@@ -165,7 +165,7 @@ defmodule Task do
{Task, fn -> :some_work end}
], strategy: :one_for_one)
This is often useful when you need to execute code concurrently while
This is often useful when you need to execute some steps while
setting up your supervision tree. For example: to warm up caches,
log the initialization status, and such.
@@ -196,17 +196,15 @@ defmodule Task do
and `Task.start_link/1` are for fire-and-forget tasks, where you don't
care about the results or if it completes successfully or not.
Keep in mind the Supervisor will not wait for the task to finish running
before starting the next child or returning. If you need synchronous
initialization, then either use an `Agent` or a `GenServer`.
> #### `use Task` {: .info}
>
> When you `use Task`, the `Task` module will define a
> `child_spec/1` function, so your module can be used
> as a child in a supervision tree.
The generated `child_spec/1` can be customized with the following options:
`use Task` defines a `child_spec/1` function, allowing the
defined module to be put under a supervision tree. The generated
`child_spec/1` can be customized with the following options:
* `:id` - the child specification identifier, defaults to the current module
* `:restart` - when the child should be restarted, defaults to `:temporary`
@@ -230,7 +228,7 @@ defmodule Task do
## Ancestor and Caller Tracking
Whenever you start a new process, Elixir annotates the process with the parent
Whenever you start a new process, Elixir annotates the parent of that process
through the `$ancestors` key in the process dictionary. This is often used to
track the hierarchy inside a supervision tree.
@@ -1248,10 +1246,9 @@ defmodule Task do
{ref, nil}
end)
ref_count = map_size(refs)
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
timeout = Keyword.get(opts, :timeout, 5_000)
limit = min(Keyword.get(opts, :limit, ref_count), ref_count)
limit = Keyword.get(opts, :limit, map_size(refs))
timeout_ref = make_ref()
timer_ref =
+5 -5
View File
@@ -54,7 +54,7 @@ defmodule Task.Supervised do
# 4. The calling process has not exited and so does not send its monitor reference
# 5. The spawned task waits forever for the monitor reference so it can begin
#
# We have solved this by specifying a timeout of 5000 milliseconds.
# We have solved this by specifying a timeout of 5000 seconds.
# Given no work is done in the client between the task start and
# sending the reference, 5000 should be enough to not raise false
# negatives unless the nodes are indeed not available.
@@ -122,8 +122,8 @@ defmodule Task.Supervised do
function: fun,
args: args,
reason: {log_value(kind, value), __STACKTRACE__},
# using :proc_lib over Process because we want the :undefined default, not nil
process_label: :proc_lib.get_label(self())
# TODO use Process.get_label/0 when we require Erlang/OTP 27+
process_label: Process.get(:"$process_label", :undefined)
}
},
%{
@@ -162,7 +162,7 @@ defmodule Task.Supervised do
~c"** When function == ~p~n" ++
~c"** arguments == ~p~n" ++ ~c"** Reason for termination == ~n" ++ ~c"** ~p~n"
terms = [starter, fun, args, get_reason(reason)]
terms = [name, fun, args, get_reason(reason)]
{message, terms} =
case process_label do
@@ -173,7 +173,7 @@ defmodule Task.Supervised do
message =
~c"** Task ~p terminating~n" ++ message
{message, [name | terms]}
{message, [starter | terms]}
end
defp get_from({node, pid_or_name, _pid}) when node == node(), do: pid_or_name
+2 -2
View File
@@ -502,8 +502,8 @@ defmodule Task.Supervisor do
Returns all children PIDs except those that are restarting.
Note that calling this function when supervising a large number
of children under low memory conditions can bring the system down due to an
out of memory error.
of children under low memory conditions can cause an out of memory
exception.
"""
@spec children(Supervisor.supervisor()) :: [pid]
def children(supervisor) do
+17 -53
View File
@@ -8,12 +8,10 @@ defmodule URI do
This module provides functions for working with URIs (for example, parsing
URIs or encoding query strings). The functions in this module are implemented
according to [RFC 3986](https://tools.ietf.org/html/rfc3986) and it also
provides additional functionality for handling "application/x-www-form-urlencoded"
segments.
according to [RFC 3986](https://tools.ietf.org/html/rfc3986).
Additionally, the Erlang [`:uri_string` module](`:uri_string`) provides additional
functionality such as RFC 3986 compliant URI normalization.
Additionally, the Erlang [`:uri_string` module](`:uri_string`) provides certain functionalities,
such as RFC 3986 compliant URI normalization.
"""
@doc """
@@ -24,12 +22,6 @@ defmodule URI do
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
The fields contain the encoded URI components as they appear in the URI
itself. For example, a slash inside the userinfo must be stored as `%2F`,
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
percent-encoded sequences in those fields, and functions such as `to_string/1`
expects those fields to already be encoded as needed. Whenever setting or
modifying the fields directly, you must encode them accordingly.
Note the `authority` field is deprecated. `parse/1` will still
populate it for backwards compatibility but you should generally
@@ -38,11 +30,6 @@ defmodule URI do
@derive {Inspect, optional: [:authority]}
defstruct [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment]
# We don't use opaque because URIs can be inlined,
# either via module attributes or by the compiler.
@typedoc deprecated: "The authority field is deprecated"
@type authority :: nil | binary
@type t :: %__MODULE__{
scheme: nil | binary,
authority: authority,
@@ -54,6 +41,9 @@ defmodule URI do
fragment: nil | binary
}
@typedoc deprecated: "The authority field is deprecated"
@opaque authority :: nil | binary
defmodule Error do
@moduledoc """
An exception raised when an error occurs when a `URI` is invalid.
@@ -511,9 +501,9 @@ defmodule URI do
defp hex_to_dec(_n), do: nil
@doc """
Creates a new URI struct by parsing and validating a string or from an existing URI.
Creates a new URI struct from a URI or a string.
If a `%URI{}` struct is given, it returns `{:ok, uri}` as is. If a string is
If a `%URI{}` struct is given, it returns `{:ok, uri}`. If a string is
given, it will parse and validate it. If the string is valid, it returns
`{:ok, uri}`, otherwise it returns `{:error, part}` with the invalid part
of the URI. For parsing URIs without further validation, see `parse/1`.
@@ -526,16 +516,6 @@ defmodule URI do
for the URI's scheme is used for the `:port` field. The scheme is also
normalized to lowercase.
> #### Browser compatibility {: .warning}
>
> This function does not follow the same parsing rules as browsers.
> Browsers adhere to the WHATWG URL standard, while this function
> implements RFC 3986. You should expect different behaviours between
> them and this function will often reject URLs that are accepted by
> browsers (as they tend to be permissive). Common examples include
> unencoded square brackets in query strings (such as `foo[bar]=baz`)
> and backslashes used as path separators.
## Examples
iex> URI.new("https://elixir-lang.org/")
@@ -709,26 +689,18 @@ defmodule URI do
This function can parse both absolute and relative URLs. You can check
if a URI is absolute or relative by checking if the `scheme` field is
nil or not.
This function expects both absolute and relative URIs to be well-formed
and does not perform any validation, it simply breaks the URL into parts.
Use `new/1` if you want to validate the URI fields after parsing.
nil or not. Furthermore, this function expects both absolute and
relative URIs to be well-formed and does not perform any validation.
See the "Examples" section below. Use `new/1` if you want to validate
the URI fields after parsing.
When a URI is given without a port, the value returned by `URI.default_port/1`
for the URI's scheme is used for the `:port` field. The scheme is also
normalized to lowercase.
If a `URI` struct is given to this function, this function returns it
If a `%URI{}` struct is given to this function, this function returns it
unmodified.
> #### Browser compatibility {: .warning}
>
> This function does not follow the same parsing rules as browsers.
> Browsers adhere to the WHATWG URL standard, while this function
> implements RFC 3986. You should expect different behaviours between
> them, especially around corner cases.
> #### `:authority` field {: .info}
>
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
@@ -781,9 +753,8 @@ defmodule URI do
userinfo: nil
}
In contrast to `URI.new/1`, this function will accept poorly-formed
URIs and break them into components, as it does not perform validation.
For example:
In contrast to `URI.new/1`, this function will parse poorly-formed
URIs, for example:
iex> URI.parse("/invalid_greater_than_in_path/>")
%URI{
@@ -797,8 +768,8 @@ defmodule URI do
}
Another example is a URI with brackets in query strings. It is accepted
by `parse/1` for the same reasons as above (no validation), but it will
be refused by `new/1`:
by `parse/1`, it is commonly accepted by browsers, but it will be refused
by `new/1`:
iex> URI.parse("/?foo[bar]=baz")
%URI{
@@ -811,9 +782,6 @@ defmodule URI do
userinfo: nil
}
Generally speaking, square brackets are strictly invalid even in the WHATWG
URL standard used by browsers, but browsers tend to be permissive and often
allow invalid characters to pass through.
"""
@spec parse(t | binary) :: t
def parse(%URI{} = uri), do: uri
@@ -903,9 +871,6 @@ defmodule URI do
@doc """
Returns the string representation of the given [URI struct](`t:t/0`).
This function assembles the URI components into a string, assuming each
field is valid and escaped as done by `parse/1` and `new/1`.
## Examples
iex> uri = URI.parse("http://google.com")
@@ -973,7 +938,6 @@ defmodule URI do
end
defp merge_paths(nil, rel_path), do: merge_paths("/", rel_path)
defp merge_paths("", rel_path), do: merge_paths("/", rel_path)
defp merge_paths(_, "/" <> _ = rel_path), do: remove_dot_segments_from_path(rel_path)
defp merge_paths(base_path, rel_path) do
+1 -23
View File
@@ -18,16 +18,12 @@ defmodule Version do
MAJOR.MINOR.PATCH
Each numeric component is limited to at most 14 digits.
Pre-releases are supported by optionally appending a hyphen and a series of
period-separated identifiers immediately following the patch version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
"1.0.0-alpha.3"
Numeric pre-release identifiers are also limited to at most 14 digits.
Build information can be added by appending a plus sign and a series of
dot-separated identifiers immediately following the patch or pre-release version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
@@ -48,11 +44,6 @@ defmodule Version do
# Anything later than 2.0.0
"> 2.0.0"
You can skip the operator, which is equivalent to `==`:
# Only version 2.0.0
"2.0.0"
Requirements also support `and` and `or` for complex conditions:
# 2.0.0 and later until 2.1.0
@@ -524,8 +515,6 @@ defmodule Version do
defmodule Parser do
@moduledoc false
@max_numeric_component_digits 14
operators = [
{">=", :>=},
{"<=", :<=},
@@ -627,9 +616,7 @@ defmodule Version do
defp require_digits(nil), do: :error
defp require_digits(string) do
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
do: :error,
else: parse_digits(string, "")
if leading_zero?(string), do: :error, else: parse_digits(string, "")
end
defp leading_zero?(<<?0, _, _::binary>>), do: true
@@ -657,11 +644,6 @@ defmodule Version do
end
end
defp convert_parts_to_integer([part | rest], acc)
when byte_size(part) > @max_numeric_component_digits do
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
end
defp convert_parts_to_integer([part | rest], acc) do
case parse_digits(part, "") do
{:ok, integer} ->
@@ -680,10 +662,6 @@ defmodule Version do
{:ok, Enum.reverse(acc)}
end
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
defp all_digits?(<<>>), do: true
defp all_digits?(_other), do: false
defp valid_identifier?(<<char, rest::binary>>)
when char in ?0..?9
when char in ?a..?z

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